- 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>
1222 lines
33 KiB
Markdown
1222 lines
33 KiB
Markdown
# 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)
|