Files
orion/docs/frontend/shared/jinja-macros.md
Samir Boulahtit b0a40200c1
Some checks failed
CI / ruff (push) Successful in 9s
CI / architecture (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / audit (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / pytest (push) Has been cancelled
docs: add all missing pages to mkdocs nav and fix absolute link
- 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>
2026-02-13 22:07:50 +01:00

34 KiB
Raw Permalink Blame History

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: 24px
  • sm: 32px
  • md: 40px
  • lg: 48px
  • xl: 56px
  • 2xl: 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_var is true
  • Shows spinner icon and loading_label when loading
  • Uses external state (no x-data wrapper - 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:

  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.htmljob_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 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.

{{ 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 %}