- Add 32 pages to nav: architecture (9), modules (1), migrations (1), testing (3), proposals (8), archive (11) - Fix absolute link in jinja-macros.md that mkdocs couldn't validate - Exclude mkdocs.yml from check-yaml hook (uses !!python/name tags) - Result: mkdocs build with zero warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
34 KiB
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
{% 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 | 5 | Loading states, error alerts, toasts |
| avatars.html | 7 | User avatars with status indicators |
| badges.html | 8 | Status badges, role badges, counts |
| buttons.html | 11 | Primary, secondary, danger, icon buttons |
| cards.html | 7 | Stats cards, info cards, filter cards |
| charts.html | 7 | Chart.js integration with Card wrappers |
| datepicker.html | 6 | Flatpickr date/time pickers |
| dropdowns.html | 8 | Dropdown menus, context menus |
| forms.html | 12 | Input fields, selects, checkboxes |
| headers.html | 6 | Page headers, breadcrumbs, tabs |
| modals.html | 5 + patterns | Modal dialogs, slide-overs, detail patterns |
| pagination.html | 2 | Table pagination controls |
| tables.html | 10 | Table wrappers, cells, empty states |
Total: 94 macros
Alerts
File: shared/macros/alerts.html
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert, toast %}
loading_state
Shows a centered loading spinner with message.
{{ 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.
{{ error_state(show_condition='error', error_var='errorMessage') }}
alert
Static alert box with variants.
{{ 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).
{{ toast('Item saved!', 'success') }}
Avatars
File: shared/macros/avatars.html
{% from 'shared/macros/avatars.html' import avatar, avatar_with_status, avatar_initials, avatar_group %}
avatar
Basic avatar component.
{# 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: 24pxsm: 32pxmd: 40pxlg: 48pxxl: 56px2xl: 64px
avatar_with_status
Avatar with online/offline indicator.
{{ 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.
{{ 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.
{% 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.
{{ user_avatar_card(src='user.avatar', name='user.name', subtitle='user.role', size='md') }}
Badges
File: shared/macros/badges.html
{% from 'shared/macros/badges.html' import badge, status_badge, role_badge, severity_badge %}
badge
Generic badge.
{{ 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.
{{ status_badge(condition='item.is_active', true_text='Active', false_text='Inactive') }}
role_badge
User role badge.
{{ role_badge(role='item.role') }}
Automatically colors: admin=red, manager=purple, staff=blue, viewer=gray
severity_badge
Severity level badge.
{{ severity_badge(severity='violation.severity') }}
Levels: critical=red, error=orange, warning=yellow, info=blue
order_status_badge
Order status badge.
{{ order_status_badge(status='order.status') }}
Statuses: pending, processing, shipped, delivered, cancelled, refunded
count_badge
Numeric count badge (notification style).
{{ count_badge(count='notifications.length', max=99) }}
Buttons
File: shared/macros/buttons.html
{% from 'shared/macros/buttons.html' import btn, btn_primary, btn_secondary, btn_danger, action_button %}
btn
Base button macro.
{{ 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
{{ 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.
{{ 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.
{% 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.
{{ back_button(href='/admin/items', label='Back to List') }}
submit_button
Form submit button with loading state.
{{ submit_button(label='Save Changes', loading_var='saving', loading_text='Saving...') }}
Cards
File: shared/macros/cards.html
{% from 'shared/macros/cards.html' import stat_card, card, info_card, filter_card %}
stat_card
Statistics card with icon and value.
{{ 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.
{% 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.
{% call card(title='User Details', subtitle='View and edit user information') %}
<p>Card content here</p>
{% endcall %}
info_card
Card with icon and description.
{{ info_card(icon='document', title='Documentation', description='View the full docs', color='blue', href='/docs') }}
filter_card
Card for search and filter controls.
{% 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.
{% from 'shared/macros/charts.html' import chart_card, line_chart, bar_chart, doughnut_chart, chart_config_script %}
Loading Chart.js
In your page template:
{% 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.
{{ 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
{{ line_chart('revenueChart', height='250px') }}
{{ bar_chart('ordersChart', height='300px') }}
{{ doughnut_chart('categoryChart', size='200px') }}
chart_config_script
Include Chart.js configuration helpers.
{{ 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.
{% from 'shared/macros/datepicker.html' import datepicker, daterange_picker, datetime_picker, time_picker %}
Loading Flatpickr
In your page template:
{% 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.
{{ 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.
{{ daterange_picker('dateRange', 'filters.dateRange', label='Date Range') }}
datetime_picker
Date and time picker.
{{ 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.
{{ time_picker('startTime', 'formData.startTime', label='Start Time') }}
Dropdowns
File: shared/macros/dropdowns.html
{% from 'shared/macros/dropdowns.html' import dropdown, action_dropdown, context_menu, dropdown_item, dropdown_divider %}
dropdown
Button with dropdown menu.
{% 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 |
action_dropdown
Dropdown with loading/disabled state support for action buttons. Uses external Alpine.js state from parent component.
{% call action_dropdown(
label='Run Scan',
loading_label='Scanning...',
open_var='scanDropdownOpen',
loading_var='scanning',
icon='search'
) %}
{{ dropdown_item('Run All', 'runScan("all"); scanDropdownOpen = false') }}
{{ dropdown_item('Run Selected', 'runScan("selected"); scanDropdownOpen = false') }}
{% endcall %}
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Button label |
loading_label |
string | 'Loading...' |
Label shown when loading |
open_var |
string | 'isDropdownOpen' |
Alpine.js variable for open state |
loading_var |
string | 'isLoading' |
Alpine.js variable for loading/disabled state |
icon |
string | none |
Button icon (before label) |
position |
string | 'right' |
'left', 'right' |
variant |
string | 'primary' |
'primary', 'secondary' |
width |
string | 'w-48' |
Dropdown width |
Features:
- Button is disabled when
loading_varis true - Shows spinner icon and
loading_labelwhen loading - Uses external state (no
x-datawrapper - expects parent component to define variables)
context_menu
Three-dot context menu (for table rows).
{% 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.
{{ 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.
{% 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
{% 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.
{{ 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.
{{ 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.
{{ 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.
{{ form_select('Status', 'formData.status', [
{'value': 'active', 'label': 'Active'},
{'value': 'inactive', 'label': 'Inactive'}
], required=true) }}
form_select_dynamic
Select with Alpine.js options.
{{ form_select_dynamic('Category', 'formData.categoryId', 'categories', value_key='id', label_key='name') }}
form_textarea
Textarea input.
{{ form_textarea('Description', 'formData.description', rows=4, maxlength=500) }}
form_checkbox
Checkbox input.
{{ form_checkbox('Subscribe to newsletter', 'formData.subscribe', help='We will send weekly updates') }}
form_toggle
Toggle switch.
{{ form_toggle('Enable notifications', 'formData.notifications') }}
form_radio_group
Radio button group.
{{ 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.
{{ search_input(x_model='filters.search', placeholder='Search items...', on_input='debouncedSearch()') }}
filter_select
Compact filter select (no label).
{{ 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.
{{ file_input('Upload Image', 'image', accept='image/*', max_size=5, on_change='handleFileSelect($event)') }}
Headers
File: shared/macros/headers.html
{% from 'shared/macros/headers.html' import page_header, section_header, breadcrumbs, tab_header %}
page_header
Page title with optional action button.
{{ 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.
{{ page_header_dynamic(title_var='pageTitle', back_url='/admin/items') }}
section_header
Section header within a page.
{{ section_header('Account Settings', subtitle='Update your account preferences', icon='cog', action_label='Edit', action_onclick='openSettings()') }}
breadcrumbs
Navigation breadcrumbs.
{{ breadcrumbs([
{'label': 'Home', 'url': '/admin'},
{'label': 'Users', 'url': '/admin/users'},
{'label': 'John Doe'}
]) }}
tab_header
Tab navigation.
{{ 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
{% from 'shared/macros/modals.html' import modal, confirm_modal, form_modal, slide_over %}
modal
Basic modal dialog.
{% 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.
{{ 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.
{% 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.
{% 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:
- Header - Icon with status-based coloring, title, subtitle, status badge, close button
- Stats Cards - Grid of colored stat cards (imported, updated, errors, total)
- Details Table - Key-value pairs with icons in a bordered table
- Content Sections - Message, exception, stack trace (conditional)
- 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
{# 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
{% from 'shared/macros/pagination.html' import pagination, pagination_simple %}
pagination
Full pagination with page numbers.
{{ pagination(show_condition='items.length > 0') }}
Required Alpine.js properties:
pagination.page- Current pagepagination.total- Total itemsstartIndex- First item indexendIndex- Last item indextotalPages- Total pagespageNumbers- Array with page numbers and'...'
Required methods:
previousPage()nextPage()goToPage(pageNum)
pagination_simple
Simple prev/next pagination.
{{ pagination_simple() }}
Tables
File: shared/macros/tables.html
{% 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.
{% 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.
{{ table_header(['Name', 'Email', 'Role', 'Status', 'Created', 'Actions']) }}
table_body
Styled tbody wrapper.
{% 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.
{{ 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.
{{ table_cell_avatar(image_src='item.avatar', title='item.name', subtitle='item.email') }}
table_cell_text
Simple text cell.
{{ table_cell_text(text='item.email', is_dynamic=true, truncate=true) }}
table_cell_date
Formatted date cell.
{{ table_cell_date(date_var='item.created_at') }}
table_loading_overlay
Loading overlay for table.
{{ table_loading_overlay(show_condition='loading') }}
Complete Page Example
{% 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 %}