# Jinja Macros Library

**Version:** 1.0
**Last Updated:** December 2024
**Location:** `app/templates/shared/macros/`

---

## Overview

The Jinja macros library provides reusable UI components for building consistent admin pages. All macros are built with:

- **Tailwind CSS** for styling
- **Alpine.js** for interactivity
- **Dark mode** support built-in
- **Accessibility** best practices

### Quick Start

```jinja
{% from 'shared/macros/buttons.html' import btn_primary %}
{% from 'shared/macros/forms.html' import form_input %}
{% from 'shared/macros/modals.html' import confirm_modal %}

{{ btn_primary('Save Changes', onclick='saveForm()') }}
{{ form_input('Email', 'email', 'formData.email', type='email', required=true) }}
{{ confirm_modal('deleteModal', 'Delete Item', 'Are you sure?', 'deleteItem()', 'showDeleteModal') }}
```

---

## Available Macro Files

| File | Macros | Description |
|------|--------|-------------|
| [alerts.html](#alerts) | 5 | Loading states, error alerts, toasts |
| [avatars.html](#avatars) | 7 | User avatars with status indicators |
| [badges.html](#badges) | 8 | Status badges, role badges, counts |
| [buttons.html](#buttons) | 11 | Primary, secondary, danger, icon buttons |
| [cards.html](#cards) | 7 | Stats cards, info cards, filter cards |
| [charts.html](#charts) | 7 | Chart.js integration with Card wrappers |
| [datepicker.html](#datepicker) | 6 | Flatpickr date/time pickers |
| [dropdowns.html](#dropdowns) | 8 | Dropdown menus, context menus |
| [forms.html](#forms) | 12 | Input fields, selects, checkboxes |
| [headers.html](#headers) | 6 | Page headers, breadcrumbs, tabs |
| [modals.html](#modals) | 5 + patterns | Modal dialogs, slide-overs, detail patterns |
| [pagination.html](#pagination) | 2 | Table pagination controls |
| [tables.html](#tables) | 10 | Table wrappers, cells, empty states |

**Total: 94 macros**

---

## Alerts

**File:** `shared/macros/alerts.html`

```jinja
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert, toast %}
```

### loading_state

Shows a centered loading spinner with message.

```jinja
{{ loading_state(show_condition='loading', message='Loading data...') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `show_condition` | string | `'loading'` | Alpine.js condition |
| `message` | string | `'Loading...'` | Loading message |

### error_state

Shows an error alert with icon.

```jinja
{{ error_state(show_condition='error', error_var='errorMessage') }}
```

### alert

Static alert box with variants.

```jinja
{{ alert('Operation completed successfully!', variant='success', dismissible=true) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `message` | string | required | Alert message |
| `variant` | string | `'info'` | `'success'`, `'warning'`, `'error'`, `'info'` |
| `dismissible` | bool | `false` | Show close button |
| `icon` | string | auto | Icon name |

### toast

Toast notification (auto-dismiss).

```jinja
{{ toast('Item saved!', 'success') }}
```

---

## Avatars

**File:** `shared/macros/avatars.html`

```jinja
{% from 'shared/macros/avatars.html' import avatar, avatar_with_status, avatar_initials, avatar_group %}
```

### avatar

Basic avatar component.

```jinja
{# Static image #}
{{ avatar(src='/images/user.jpg', alt='John Doe', size='lg') }}

{# Dynamic with Alpine.js #}
{{ avatar(src='user.avatar_url', alt='user.name', size='md', dynamic=true) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `src` | string | `''` | Image URL or Alpine.js expression |
| `alt` | string | `''` | Alt text |
| `size` | string | `'md'` | `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'` |
| `dynamic` | bool | `false` | Whether src is Alpine.js expression |
| `fallback_icon` | string | `'user'` | Icon when no image |

**Sizes:**
- `xs`: 24px
- `sm`: 32px
- `md`: 40px
- `lg`: 48px
- `xl`: 56px
- `2xl`: 64px

### avatar_with_status

Avatar with online/offline indicator.

```jinja
{{ avatar_with_status(src='user.avatar', status='online', size='md', dynamic=true) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `status` | string | `'online'` | `'online'`, `'offline'`, `'away'`, `'busy'` |

### avatar_initials

Avatar showing initials.

```jinja
{{ avatar_initials(initials='JD', size='md', color='purple') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `initials` | string | required | 1-2 characters |
| `color` | string | `'purple'` | `'gray'`, `'purple'`, `'blue'`, `'green'`, `'red'`, `'yellow'`, `'orange'` |

### avatar_group

Stacked avatar group.

```jinja
{% call avatar_group(max=4, total_var='team.length', size='sm') %}
    <template x-for="member in team.slice(0, 4)" :key="member.id">
        {{ avatar_group_item(src='member.avatar', dynamic=true) }}
    </template>
{% endcall %}
```

### user_avatar_card

Avatar with name and subtitle.

```jinja
{{ user_avatar_card(src='user.avatar', name='user.name', subtitle='user.role', size='md') }}
```

---

## Badges

**File:** `shared/macros/badges.html`

```jinja
{% from 'shared/macros/badges.html' import badge, status_badge, role_badge, severity_badge %}
```

### badge

Generic badge.

```jinja
{{ badge('New', variant='success', icon='check') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `text` | string | required | Badge text |
| `variant` | string | `'gray'` | `'gray'`, `'green'`, `'red'`, `'yellow'`, `'blue'`, `'purple'` |
| `icon` | string | `none` | Optional icon |
| `size` | string | `'md'` | `'sm'`, `'md'` |

### status_badge

Dynamic status badge based on boolean.

```jinja
{{ status_badge(condition='item.is_active', true_text='Active', false_text='Inactive') }}
```

### role_badge

User role badge.

```jinja
{{ role_badge(role='item.role') }}
```

Automatically colors: admin=red, manager=purple, staff=blue, viewer=gray

### severity_badge

Severity level badge.

```jinja
{{ severity_badge(severity='violation.severity') }}
```

Levels: critical=red, error=orange, warning=yellow, info=blue

### order_status_badge

Order status badge.

```jinja
{{ order_status_badge(status='order.status') }}
```

Statuses: pending, processing, shipped, delivered, cancelled, refunded

### count_badge

Numeric count badge (notification style).

```jinja
{{ count_badge(count='notifications.length', max=99) }}
```

---

## Buttons

**File:** `shared/macros/buttons.html`

```jinja
{% from 'shared/macros/buttons.html' import btn, btn_primary, btn_secondary, btn_danger, action_button %}
```

### btn

Base button macro.

```jinja
{{ btn('Click Me', variant='primary', size='md', icon='plus', onclick='doSomething()') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `label` | string | required | Button text |
| `variant` | string | `'primary'` | `'primary'`, `'secondary'`, `'danger'`, `'success'`, `'ghost'` |
| `size` | string | `'md'` | `'sm'`, `'md'`, `'lg'` |
| `icon` | string | `none` | Icon name |
| `icon_position` | string | `'left'` | `'left'`, `'right'` |
| `onclick` | string | `none` | Alpine.js click handler |
| `href` | string | `none` | Makes it a link |
| `disabled` | string | `none` | Alpine.js disabled condition |
| `loading` | string | `none` | Alpine.js loading condition |
| `type` | string | `'button'` | Button type |

### Convenience Variants

```jinja
{{ btn_primary('Save', icon='check', onclick='save()') }}
{{ btn_secondary('Cancel', onclick='cancel()') }}
{{ btn_danger('Delete', icon='trash', onclick='delete()') }}
{{ btn_success('Approve', onclick='approve()') }}
```

### action_button

Icon-only action button for tables.

```jinja
{{ action_button(icon='edit', onclick='edit(item)', variant='primary', title='Edit') }}
{{ action_button(icon='trash', onclick='delete(item)', variant='danger', title='Delete') }}
```

### action_button_group

Group of action buttons.

```jinja
{% call action_button_group() %}
    {{ action_button(icon='eye', href="'/items/' + item.id", variant='info', title='View') }}
    {{ action_button(icon='edit', onclick='edit(item)', variant='primary', title='Edit') }}
    {{ action_button(icon='trash', onclick='confirmDelete(item)', variant='danger', title='Delete') }}
{% endcall %}
```

### back_button

Back navigation button.

```jinja
{{ back_button(href='/admin/items', label='Back to List') }}
```

### submit_button

Form submit button with loading state.

```jinja
{{ submit_button(label='Save Changes', loading_var='saving', loading_text='Saving...') }}
```

---

## Cards

**File:** `shared/macros/cards.html`

```jinja
{% from 'shared/macros/cards.html' import stat_card, card, info_card, filter_card %}
```

### stat_card

Statistics card with icon and value.

```jinja
{{ stat_card(icon='users', label='Total Users', value='stats.totalUsers', color='purple') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `icon` | string | required | Icon name |
| `label` | string | required | Stat label |
| `value` | string | required | Alpine.js expression |
| `color` | string | `'orange'` | `'orange'`, `'green'`, `'blue'`, `'purple'`, `'red'`, `'yellow'`, `'teal'` |
| `format` | string | `none` | Format function (e.g., `'formatCurrency'`) |

### stats_grid

Grid container for stat cards.

```jinja
{% call stats_grid(columns=4) %}
    {{ stat_card(icon='users', label='Users', value='stats.users', color='blue') }}
    {{ stat_card(icon='shopping-cart', label='Orders', value='stats.orders', color='green') }}
    {{ stat_card(icon='currency-dollar', label='Revenue', value='stats.revenue', color='purple', format='formatCurrency') }}
    {{ stat_card(icon='chart-bar', label='Growth', value='stats.growth', color='orange') }}
{% endcall %}
```

### card

Basic card container.

```jinja
{% call card(title='User Details', subtitle='View and edit user information') %}
    <p>Card content here</p>
{% endcall %}
```

### info_card

Card with icon and description.

```jinja
{{ info_card(icon='document', title='Documentation', description='View the full docs', color='blue', href='/docs') }}
```

### filter_card

Card for search and filter controls.

```jinja
{% call filter_card() %}
    {{ search_input(x_model='filters.search', on_input='debouncedSearch()') }}
    {{ filter_select(x_model='filters.status', options=[...]) }}
{% endcall %}
```

---

## Charts

**File:** `shared/macros/charts.html`

**Prerequisites:** Requires Chart.js. See [CDN Fallback Strategy](../cdn-fallback-strategy.md).

```jinja
{% from 'shared/macros/charts.html' import chart_card, line_chart, bar_chart, doughnut_chart, chart_config_script %}
```

### Loading Chart.js

In your page template:

```jinja
{% block chartjs_script %}
{% from 'shared/includes/optional-libs.html' import chartjs_loader %}
{{ chartjs_loader() }}
{% endblock %}
```

### chart_card

Chart in a card with title and optional menu.

```jinja
{{ chart_card('salesChart', 'Monthly Sales', 'line', height='300px') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | string | required | Canvas element ID |
| `title` | string | required | Card title |
| `chart_type` | string | `'line'` | `'line'`, `'bar'`, `'doughnut'`, `'pie'` |
| `height` | string | `'300px'` | Chart height |
| `show_menu` | bool | `true` | Show dropdown menu |

### Standalone Charts

```jinja
{{ line_chart('revenueChart', height='250px') }}
{{ bar_chart('ordersChart', height='300px') }}
{{ doughnut_chart('categoryChart', size='200px') }}
```

### chart_config_script

Include Chart.js configuration helpers.

```jinja
{{ chart_config_script() }}

<script>
document.addEventListener('DOMContentLoaded', function() {
    createLineChart('salesChart',
        ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        [{
            label: 'Sales',
            data: [30, 40, 35, 50, 49, 60],
            borderColor: chartColors.purple.solid,
            backgroundColor: chartColors.purple.light
        }]
    );
});
</script>
```

**Available helper functions:**
- `createChart(id, type, data, options)`
- `createLineChart(id, labels, datasets, options)`
- `createBarChart(id, labels, datasets, options)`
- `createDoughnutChart(id, labels, data, colors, options)`

**Color presets:** `chartColors.purple`, `chartColors.blue`, `chartColors.green`, `chartColors.red`, `chartColors.yellow`, `chartColors.gray`

---

## Datepicker

**File:** `shared/macros/datepicker.html`

**Prerequisites:** Requires Flatpickr. See [CDN Fallback Strategy](../cdn-fallback-strategy.md).

```jinja
{% from 'shared/macros/datepicker.html' import datepicker, daterange_picker, datetime_picker, time_picker %}
```

### Loading Flatpickr

In your page template:

```jinja
{% block flatpickr_css %}
{% from 'shared/includes/optional-libs.html' import flatpickr_css_loader %}
{{ flatpickr_css_loader() }}
{% endblock %}

{% block flatpickr_script %}
{% from 'shared/includes/optional-libs.html' import flatpickr_loader %}
{{ flatpickr_loader() }}
{% endblock %}
```

### datepicker

Single date picker.

```jinja
{{ datepicker('startDate', 'formData.startDate', label='Start Date', required=true) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | string | required | Input ID |
| `x_model` | string | required | Alpine.js model |
| `label` | string | `none` | Field label |
| `placeholder` | string | `'Select date'` | Placeholder text |
| `format` | string | `'Y-m-d'` | Date format |
| `min_date` | string | `none` | Minimum date |
| `max_date` | string | `none` | Maximum date |
| `required` | bool | `false` | Required field |

### daterange_picker

Date range picker.

```jinja
{{ daterange_picker('dateRange', 'filters.dateRange', label='Date Range') }}
```

### datetime_picker

Date and time picker.

```jinja
{{ datetime_picker('scheduledAt', 'formData.scheduledAt', label='Schedule For', minute_increment=15) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `time_24hr` | bool | `true` | Use 24-hour format |
| `minute_increment` | int | `5` | Minute step |

### time_picker

Time-only picker.

```jinja
{{ time_picker('startTime', 'formData.startTime', label='Start Time') }}
```

---

## Dropdowns

**File:** `shared/macros/dropdowns.html`

```jinja
{% from 'shared/macros/dropdowns.html' import dropdown, context_menu, dropdown_item, dropdown_divider %}
```

### dropdown

Button with dropdown menu.

```jinja
{% call dropdown('Actions', position='right') %}
    {{ dropdown_item('Edit', onclick='edit()', icon='pencil') }}
    {{ dropdown_item('Duplicate', onclick='duplicate()', icon='document-duplicate') }}
    {{ dropdown_divider() }}
    {{ dropdown_item('Delete', onclick='delete()', icon='trash', variant='danger') }}
{% endcall %}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `label` | string | required | Button label |
| `position` | string | `'right'` | `'left'`, `'right'` |
| `variant` | string | `'secondary'` | `'primary'`, `'secondary'`, `'ghost'` |
| `icon` | string | `'chevron-down'` | Button icon |
| `width` | string | `'w-48'` | Dropdown width |

### context_menu

Three-dot context menu (for table rows).

```jinja
{% call context_menu() %}
    {{ dropdown_item('View', href="'/items/' + item.id", icon='eye') }}
    {{ dropdown_item('Edit', onclick='edit(item)', icon='pencil') }}
    {{ dropdown_divider() }}
    {{ dropdown_item('Delete', onclick='confirmDelete(item)', icon='trash', variant='danger') }}
{% endcall %}
```

### dropdown_item

Menu item.

```jinja
{{ dropdown_item('Edit', onclick='edit()', icon='pencil') }}
{{ dropdown_item('View Details', href='/details/123', icon='eye') }}
{{ dropdown_item('Delete', onclick='delete()', icon='trash', variant='danger') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `label` | string | required | Item label |
| `onclick` | string | `none` | Click handler |
| `href` | string | `none` | Link URL |
| `icon` | string | `none` | Icon name |
| `variant` | string | `'default'` | `'default'`, `'danger'` |
| `disabled` | bool | `false` | Disabled state |

### select_dropdown

Custom select dropdown.

```jinja
{% call select_dropdown(placeholder='Select status...') %}
    {{ select_option('active', 'Active') }}
    {{ select_option('inactive', 'Inactive') }}
    {{ select_option('pending', 'Pending') }}
{% endcall %}
```

---

## Forms

**File:** `shared/macros/forms.html`

```jinja
{% from 'shared/macros/forms.html' import form_input, form_select, form_textarea, form_checkbox, form_toggle, password_input, search_input %}
```

### form_input

Standard text input.

```jinja
{{ form_input('Email', 'email', 'formData.email', type='email', required=true, placeholder='Enter email') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `label` | string | required | Field label |
| `name` | string | required | Input name |
| `x_model` | string | required | Alpine.js model |
| `type` | string | `'text'` | Input type |
| `placeholder` | string | `''` | Placeholder |
| `required` | bool | `false` | Required |
| `disabled` | string | `none` | Alpine.js disabled condition |
| `error` | string | `none` | Alpine.js error message |
| `help` | string | `none` | Help text |
| `maxlength` | int | `none` | Max length |

### password_input

Password input with show/hide toggle.

```jinja
{{ password_input('Password', 'formData.password', required=true, show_strength=true) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `show_strength` | bool | `false` | Show strength indicator |
| `minlength` | int | `none` | Minimum length |

### input_with_icon

Input with icon.

```jinja
{{ input_with_icon('Website', 'formData.url', 'url', icon='globe', placeholder='https://...') }}
{{ input_with_icon('Search', 'query', 'q', icon='search', on_click_icon='search()') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `icon` | string | required | Icon name |
| `icon_position` | string | `'left'` | `'left'`, `'right'` |
| `on_click_icon` | string | `none` | Makes icon clickable |

### form_select

Select dropdown.

```jinja
{{ form_select('Status', 'formData.status', [
    {'value': 'active', 'label': 'Active'},
    {'value': 'inactive', 'label': 'Inactive'}
], required=true) }}
```

### form_select_dynamic

Select with Alpine.js options.

```jinja
{{ form_select_dynamic('Category', 'formData.categoryId', 'categories', value_key='id', label_key='name') }}
```

### form_textarea

Textarea input.

```jinja
{{ form_textarea('Description', 'formData.description', rows=4, maxlength=500) }}
```

### form_checkbox

Checkbox input.

```jinja
{{ form_checkbox('Subscribe to newsletter', 'formData.subscribe', help='We will send weekly updates') }}
```

### form_toggle

Toggle switch.

```jinja
{{ form_toggle('Enable notifications', 'formData.notifications') }}
```

### form_radio_group

Radio button group.

```jinja
{{ form_radio_group('Priority', 'priority', 'formData.priority', [
    {'value': 'low', 'label': 'Low'},
    {'value': 'medium', 'label': 'Medium'},
    {'value': 'high', 'label': 'High'}
], inline=true) }}
```

### search_input

Search input with icon.

```jinja
{{ search_input(x_model='filters.search', placeholder='Search items...', on_input='debouncedSearch()') }}
```

### filter_select

Compact filter select (no label).

```jinja
{{ filter_select(x_model='filters.status', options=[
    {'value': 'active', 'label': 'Active'},
    {'value': 'inactive', 'label': 'Inactive'}
], placeholder='All Statuses', on_change='applyFilters()') }}
```

### file_input

Drag and drop file upload.

```jinja
{{ file_input('Upload Image', 'image', accept='image/*', max_size=5, on_change='handleFileSelect($event)') }}
```

---

## Headers

**File:** `shared/macros/headers.html`

```jinja
{% from 'shared/macros/headers.html' import page_header, section_header, breadcrumbs, tab_header %}
```

### page_header

Page title with optional action button.

```jinja
{{ page_header('User Management', subtitle='Manage all users', action_label='Add User', action_url='/admin/users/create', action_icon='plus') }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `title` | string | required | Page title |
| `subtitle` | string | `none` | Subtitle |
| `action_label` | string | `none` | Action button text |
| `action_url` | string | `none` | Action button URL |
| `action_onclick` | string | `none` | Action click handler |
| `action_icon` | string | `'plus'` | Action button icon |
| `back_url` | string | `none` | Back button URL |

### page_header_dynamic

Page header with Alpine.js title.

```jinja
{{ page_header_dynamic(title_var='pageTitle', back_url='/admin/items') }}
```

### section_header

Section header within a page.

```jinja
{{ section_header('Account Settings', subtitle='Update your account preferences', icon='cog', action_label='Edit', action_onclick='openSettings()') }}
```

### breadcrumbs

Navigation breadcrumbs.

```jinja
{{ breadcrumbs([
    {'label': 'Home', 'url': '/admin'},
    {'label': 'Users', 'url': '/admin/users'},
    {'label': 'John Doe'}
]) }}
```

### tab_header

Tab navigation.

```jinja
{{ tab_header([
    {'id': 'general', 'label': 'General', 'icon': 'cog'},
    {'id': 'security', 'label': 'Security', 'icon': 'shield-check'},
    {'id': 'notifications', 'label': 'Notifications', 'icon': 'bell'}
], active_var='activeTab') }}
```

---

## Modals

**File:** `shared/macros/modals.html`

```jinja
{% from 'shared/macros/modals.html' import modal, confirm_modal, form_modal, slide_over %}
```

### modal

Basic modal dialog.

```jinja
{% call modal('editModal', 'Edit Item', show_var='isEditModalOpen', size='lg') %}
    <p>Modal content here</p>
{% endcall %}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `id` | string | required | Modal ID |
| `title` | string | required | Modal title |
| `show_var` | string | `'isModalOpen'` | Alpine.js visibility variable |
| `size` | string | `'md'` | `'sm'`, `'md'`, `'lg'`, `'xl'`, `'full'` |
| `show_close` | bool | `true` | Show close button |
| `show_footer` | bool | `true` | Show footer |
| `close_on_backdrop` | bool | `true` | Close on backdrop click |
| `close_on_escape` | bool | `true` | Close on Escape key |

### confirm_modal

Confirmation dialog for destructive actions.

```jinja
{{ confirm_modal(
    'deleteModal',
    'Delete User',
    'Are you sure you want to delete this user? This action cannot be undone.',
    'deleteUser()',
    'isDeleteModalOpen',
    confirm_text='Delete',
    variant='danger'
) }}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `confirm_action` | string | required | Alpine.js action on confirm |
| `confirm_text` | string | `'Confirm'` | Confirm button text |
| `cancel_text` | string | `'Cancel'` | Cancel button text |
| `variant` | string | `'danger'` | `'danger'`, `'warning'`, `'info'` |

### form_modal

Modal optimized for forms with loading state.

```jinja
{% call form_modal('createModal', 'Create Item', submit_action='createItem()', loading_var='saving') %}
    {{ form_input('Name', 'name', 'formData.name', required=true) }}
    {{ form_textarea('Description', 'formData.description') }}
{% endcall %}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `submit_action` | string | `'submitForm()'` | Form submit handler |
| `submit_text` | string | `'Save'` | Submit button text |
| `loading_var` | string | `'saving'` | Loading state variable |
| `loading_text` | string | `'Saving...'` | Loading button text |

### slide_over

Side panel that slides in from the right.

```jinja
{% call slide_over('detailsPanel', 'Item Details', show_var='isPanelOpen', width='lg') %}
    <div class="space-y-4">
        <p>Panel content here</p>
    </div>
{% endcall %}
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `width` | string | `'md'` | `'sm'`, `'md'`, `'lg'`, `'xl'` |

### Details Modal Pattern (Inline)

For complex detail views (job details, log entries, etc.), use inline modals with this pattern structure:

**Structure:**
1. **Header** - Icon with status-based coloring, title, subtitle, status badge, close button
2. **Stats Cards** - Grid of colored stat cards (imported, updated, errors, total)
3. **Details Table** - Key-value pairs with icons in a bordered table
4. **Content Sections** - Message, exception, stack trace (conditional)
5. **Footer** - Close button with proper styling

**Example implementations:**
- Job Details Modal: `app/templates/shared/macros/modals.html` → `job_details_modal`
- Log Details Modal: `app/templates/admin/logs.html` (inline)

**Key Features:**
- Level-based icon and color theming (success=green, warning=yellow, error=red, critical=purple)
- Stats cards grid with color-coded backgrounds
- Table layout with icon-labeled rows
- Conditional sections (exception, stack trace) with `x-show`
- Copy-to-clipboard for stack traces
- Dark mode support throughout

```jinja
{# Example structure #}
<div x-show="selectedItem" class="fixed inset-0 z-50 ...">
    <div class="bg-white dark:bg-gray-800 rounded-lg ...">
        {# Header with Icon and Badge #}
        <div class="px-6 py-4 border-b ...">
            <div class="flex items-center justify-between">
                <div class="flex items-center gap-3">
                    <div class="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
                        <span class="text-green-600" x-html="$icon('check-circle', 'w-6 h-6')"></span>
                    </div>
                    <div>
                        <h3 class="text-lg font-semibold">Title</h3>
                        <p class="text-sm text-gray-500">Subtitle</p>
                    </div>
                </div>
                <div class="flex items-center gap-3">
                    <span class="px-3 py-1 text-sm font-semibold rounded-full bg-green-100 text-green-800">Status</span>
                    <button @click="selectedItem = null">×</button>
                </div>
            </div>
        </div>

        {# Body: Stats Cards + Details Table + Content Sections #}
        <div class="p-6 overflow-y-auto ...">
            {# Stats Cards Grid #}
            <div class="grid grid-cols-4 gap-3">
                <div class="p-3 bg-green-50 rounded-lg text-center">
                    <p class="text-2xl font-bold text-green-600">150</p>
                    <p class="text-xs text-green-700">Imported</p>
                </div>
                <!-- More cards... -->
            </div>

            {# Details Table #}
            <div class="overflow-hidden border rounded-lg">
                <table class="min-w-full">
                    <tbody>
                        <tr>
                            <td class="px-4 py-3 bg-gray-50 w-1/3">
                                <div class="flex items-center gap-2">
                                    <span x-html="$icon('user', 'w-4 h-4')"></span>
                                    Field Name
                                </div>
                            </td>
                            <td class="px-4 py-3">Value</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>

        {# Footer #}
        <div class="px-6 py-4 border-t bg-gray-50">
            <div class="flex justify-end">
                <button @click="selectedItem = null">Close</button>
            </div>
        </div>
    </div>
</div>
```

See the [Components Library](/admin/components#modals) for live examples.

---

## Pagination

**File:** `shared/macros/pagination.html`

```jinja
{% from 'shared/macros/pagination.html' import pagination, pagination_simple %}
```

### pagination

Full pagination with page numbers.

```jinja
{{ pagination(show_condition='items.length > 0') }}
```

**Required Alpine.js properties:**
- `pagination.page` - Current page
- `pagination.total` - Total items
- `startIndex` - First item index
- `endIndex` - Last item index
- `totalPages` - Total pages
- `pageNumbers` - Array with page numbers and `'...'`

**Required methods:**
- `previousPage()`
- `nextPage()`
- `goToPage(pageNum)`

### pagination_simple

Simple prev/next pagination.

```jinja
{{ pagination_simple() }}
```

---

## Tables

**File:** `shared/macros/tables.html`

```jinja
{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_body, table_empty_state, table_cell_avatar, table_cell_date %}
```

### table_wrapper

Table container with overflow handling.

```jinja
{% call table_wrapper() %}
    {{ table_header(['Name', 'Email', 'Status', 'Actions']) }}
    <tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
        <!-- rows -->
    </tbody>
{% endcall %}
```

### table_header

Table header row.

```jinja
{{ table_header(['Name', 'Email', 'Role', 'Status', 'Created', 'Actions']) }}
```

### table_body

Styled tbody wrapper.

```jinja
{% call table_body() %}
    <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">
            <!-- cells -->
        </tr>
    </template>
{% endcall %}
```

### table_empty_state

Empty state message.

```jinja
{{ table_empty_state(colspan=6, icon='inbox', title='No users found', message='Create your first user to get started') }}
```

### table_cell_avatar

Cell with avatar and text.

```jinja
{{ table_cell_avatar(image_src='item.avatar', title='item.name', subtitle='item.email') }}
```

### table_cell_text

Simple text cell.

```jinja
{{ table_cell_text(text='item.email', is_dynamic=true, truncate=true) }}
```

### table_cell_date

Formatted date cell.

```jinja
{{ table_cell_date(date_var='item.created_at') }}
```

### table_loading_overlay

Loading overlay for table.

```jinja
{{ table_loading_overlay(show_condition='loading') }}
```

---

## Complete Page Example

```jinja
{% extends "admin/base.html" %}

{% from 'shared/macros/headers.html' import page_header, breadcrumbs %}
{% from 'shared/macros/cards.html' import stat_card, stats_grid, filter_card %}
{% from 'shared/macros/forms.html' import search_input, filter_select %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_body, table_empty_state, table_cell_avatar %}
{% from 'shared/macros/badges.html' import status_badge, role_badge %}
{% from 'shared/macros/buttons.html' import action_button %}
{% from 'shared/macros/pagination.html' import pagination %}
{% from 'shared/macros/modals.html' import confirm_modal %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}

{% block content %}
{{ breadcrumbs([{'label': 'Admin', 'url': '/admin'}, {'label': 'Users'}]) }}
{{ page_header('User Management', action_label='Add User', action_url='/admin/users/create') }}

{# Stats #}
{% call stats_grid() %}
    {{ stat_card('users', 'Total Users', 'stats.total', 'blue') }}
    {{ stat_card('check-circle', 'Active', 'stats.active', 'green') }}
    {{ stat_card('x-circle', 'Inactive', 'stats.inactive', 'red') }}
    {{ stat_card('shield-check', 'Admins', 'stats.admins', 'purple') }}
{% endcall %}

{# Filters #}
{% call filter_card() %}
    {{ search_input(x_model='filters.search', on_input='debouncedSearch()') }}
    {{ filter_select(x_model='filters.status', options=[
        {'value': 'active', 'label': 'Active'},
        {'value': 'inactive', 'label': 'Inactive'}
    ], on_change='loadItems()') }}
{% endcall %}

{# Loading/Error States #}
{{ loading_state() }}
{{ error_state() }}

{# Table #}
<div x-show="!loading && !error">
    {% call table_wrapper() %}
        {{ table_header(['User', 'Role', 'Status', 'Created', 'Actions']) }}
        {% call table_body() %}
            {{ table_empty_state(5, show_condition='items.length === 0') }}
            <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">
                    {{ table_cell_avatar(image_src='item.avatar', title='item.name', subtitle='item.email') }}
                    <td class="px-4 py-3">{{ role_badge(role='item.role') }}</td>
                    <td class="px-4 py-3">{{ status_badge(condition='item.is_active') }}</td>
                    <td class="px-4 py-3 text-sm" x-text="formatDate(item.created_at)"></td>
                    <td class="px-4 py-3">
                        <div class="flex items-center space-x-2">
                            {{ action_button(icon='eye', href="'/admin/users/' + item.id", variant='info') }}
                            {{ action_button(icon='edit', href="'/admin/users/' + item.id + '/edit'", variant='primary') }}
                            {{ action_button(icon='trash', onclick='confirmDelete(item)', variant='danger') }}
                        </div>
                    </td>
                </tr>
            </template>
        {% endcall %}
    {% endcall %}
    {{ pagination() }}
</div>

{# Delete Confirmation Modal #}
{{ confirm_modal('deleteModal', 'Delete User', 'Are you sure you want to delete this user?', 'deleteUser()', 'showDeleteModal') }}
{% endblock %}
```

---

## Related Documentation

- [UI Components (HTML Reference)](ui-components.md)
- [Pagination Guide](pagination.md)
- [CDN Fallback Strategy](../cdn-fallback-strategy.md)
- [Icons Guide](../../development/icons-guide.md)
