Files
orion/docs/frontend/shared/jinja-macros.md
Samir Boulahtit a40c88dcea docs: add details modal and log modal patterns to component library
- Add Details Modal (Table Layout) example to components page
  - Shows header with icon and status badge
  - Stats cards grid (imported, updated, errors, total)
  - Key-value table with icon-labeled rows
- Add Log Details Modal example with live demo
  - Level-based coloring (warning=yellow, error=red, critical=purple)
  - Message, exception, and stack trace sections
  - Copy-to-clipboard for stack traces
  - Both error and warning log demo buttons
- Update jinja-macros.md with Details Modal Pattern documentation
  - Document the pattern structure and key features
  - Link to components library for live examples
- Add Alpine.js state variables for new modal demos

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 18:01:21 +01:00

1222 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)