feat: add shared Jinja macros for reusable UI components
Add comprehensive macro library in app/templates/shared/macros/: - pagination.html: pagination(), pagination_simple() - alerts.html: loading_state(), error_state(), alert(), toast() - badges.html: badge(), status_badge(), role_badge(), severity_badge() - buttons.html: btn(), btn_primary(), btn_danger(), action_button() - forms.html: form_input(), form_select(), form_textarea(), form_toggle() - tables.html: table_wrapper(), table_header(), table_empty_state() - cards.html: stat_card(), card(), info_card(), filter_card() - headers.html: page_header(), section_header(), breadcrumbs() These macros standardize TailAdmin styling with Alpine.js integration and dark mode support, reducing code duplication across templates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
178
app/templates/shared/macros/alerts.html
Normal file
178
app/templates/shared/macros/alerts.html
Normal file
@@ -0,0 +1,178 @@
|
||||
{#
|
||||
Alert & Loading State Macros
|
||||
============================
|
||||
Reusable components for alerts, loading states, and notifications.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert %}
|
||||
{{ loading_state('Loading vendors...') }}
|
||||
{{ error_state('Error loading data', 'error') }}
|
||||
{{ alert('success', 'Success!', 'Your changes have been saved.') }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Loading State
|
||||
=============
|
||||
Shows a centered loading spinner with message.
|
||||
|
||||
Parameters:
|
||||
- message: Text to display below the spinner (default: 'Loading...')
|
||||
- show_condition: Alpine.js condition for x-show (default: 'loading')
|
||||
#}
|
||||
{% macro loading_state(message='Loading...', show_condition='loading') %}
|
||||
<div x-show="{{ show_condition }}" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ message }}</p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Error State
|
||||
===========
|
||||
Shows an error message box with icon.
|
||||
|
||||
Parameters:
|
||||
- title: Error title (default: 'Error')
|
||||
- error_var: Alpine.js variable containing error message (default: 'error')
|
||||
- show_condition: Alpine.js condition for x-show (default: 'error && !loading')
|
||||
#}
|
||||
{% macro error_state(title='Error', error_var='error', show_condition='error && !loading') %}
|
||||
<div x-show="{{ show_condition }}" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">{{ title }}</p>
|
||||
<p class="text-sm" x-text="{{ error_var }}"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Alert
|
||||
=====
|
||||
A versatile alert component for different message types.
|
||||
|
||||
Parameters:
|
||||
- type: 'success' | 'warning' | 'error' | 'info' (default: 'info')
|
||||
- title: Alert title
|
||||
- message: Alert message (can be Alpine.js expression with x-text)
|
||||
- dismissible: Whether the alert can be dismissed (default: false)
|
||||
- show_condition: Alpine.js condition for x-show (default: 'true')
|
||||
- icon: Override the default icon (optional)
|
||||
#}
|
||||
{% macro alert(type='info', title='', message='', dismissible=false, show_condition='true', icon=none) %}
|
||||
{% set colors = {
|
||||
'success': 'bg-green-100 border-green-400 text-green-700 dark:bg-green-900 dark:border-green-700 dark:text-green-200',
|
||||
'warning': 'bg-yellow-100 border-yellow-400 text-yellow-700 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200',
|
||||
'error': 'bg-red-100 border-red-400 text-red-700 dark:bg-red-900 dark:border-red-700 dark:text-red-200',
|
||||
'info': 'bg-blue-100 border-blue-400 text-blue-700 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-200'
|
||||
} %}
|
||||
{% set icons = {
|
||||
'success': 'check-circle',
|
||||
'warning': 'exclamation',
|
||||
'error': 'exclamation-circle',
|
||||
'info': 'information-circle'
|
||||
} %}
|
||||
{% set alert_icon = icon if icon else icons[type] %}
|
||||
<div x-show="{{ show_condition }}" class="mb-4 p-4 border rounded-lg flex items-start {{ colors[type] }}" role="alert">
|
||||
<span x-html="$icon('{{ alert_icon }}', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div class="flex-1">
|
||||
{% if title %}
|
||||
<p class="font-semibold">{{ title }}</p>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
<p class="text-sm">{{ message }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if dismissible %}
|
||||
<button @click="$el.parentElement.remove()" class="ml-4 hover:opacity-75 focus:outline-none" aria-label="Dismiss">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Alert with Alpine.js binding
|
||||
============================
|
||||
Alert where the message comes from an Alpine.js variable.
|
||||
|
||||
Parameters:
|
||||
- type: 'success' | 'warning' | 'error' | 'info' (default: 'info')
|
||||
- title: Alert title
|
||||
- message_var: Alpine.js variable for the message
|
||||
- show_condition: Alpine.js condition for x-show (default: 'true')
|
||||
#}
|
||||
{% macro alert_dynamic(type='info', title='', message_var='message', show_condition='true') %}
|
||||
{% set colors = {
|
||||
'success': 'bg-green-100 border-green-400 text-green-700 dark:bg-green-900 dark:border-green-700 dark:text-green-200',
|
||||
'warning': 'bg-yellow-100 border-yellow-400 text-yellow-700 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-200',
|
||||
'error': 'bg-red-100 border-red-400 text-red-700 dark:bg-red-900 dark:border-red-700 dark:text-red-200',
|
||||
'info': 'bg-blue-100 border-blue-400 text-blue-700 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-200'
|
||||
} %}
|
||||
{% set icons = {
|
||||
'success': 'check-circle',
|
||||
'warning': 'exclamation',
|
||||
'error': 'exclamation-circle',
|
||||
'info': 'information-circle'
|
||||
} %}
|
||||
<div x-show="{{ show_condition }}" class="mb-4 p-4 border rounded-lg flex items-start {{ colors[type] }}" role="alert">
|
||||
<span x-html="$icon('{{ icons[type] }}', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div class="flex-1">
|
||||
{% if title %}
|
||||
<p class="font-semibold">{{ title }}</p>
|
||||
{% endif %}
|
||||
<p class="text-sm" x-text="{{ message_var }}"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Toast Notification
|
||||
==================
|
||||
A toast notification that appears and auto-dismisses.
|
||||
|
||||
Parameters:
|
||||
- type: 'success' | 'warning' | 'error' | 'info' (default: 'success')
|
||||
- message_var: Alpine.js variable for the message
|
||||
- show_var: Alpine.js variable to control visibility
|
||||
- duration: Auto-dismiss duration in ms (default: 3000, 0 to disable)
|
||||
#}
|
||||
{% macro toast(type='success', message_var='toastMessage', show_var='showToast', duration=3000) %}
|
||||
{% set colors = {
|
||||
'success': 'bg-green-500',
|
||||
'warning': 'bg-yellow-500',
|
||||
'error': 'bg-red-500',
|
||||
'info': 'bg-blue-500'
|
||||
} %}
|
||||
{% set icons = {
|
||||
'success': 'check-circle',
|
||||
'warning': 'exclamation',
|
||||
'error': 'exclamation-circle',
|
||||
'info': 'information-circle'
|
||||
} %}
|
||||
<div
|
||||
x-show="{{ show_var }}"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-2"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform translate-y-2"
|
||||
{% if duration > 0 %}
|
||||
x-init="$watch('{{ show_var }}', value => { if (value) setTimeout(() => {{ show_var }} = false, {{ duration }}) })"
|
||||
{% endif %}
|
||||
class="fixed bottom-4 right-4 z-50 flex items-center px-4 py-3 text-white rounded-lg shadow-lg {{ colors[type] }}"
|
||||
role="alert"
|
||||
>
|
||||
<span x-html="$icon('{{ icons[type] }}', 'w-5 h-5 mr-2')"></span>
|
||||
<span x-text="{{ message_var }}"></span>
|
||||
<button @click="{{ show_var }} = false" class="ml-4 hover:opacity-75 focus:outline-none" aria-label="Dismiss">
|
||||
<span x-html="$icon('x', 'w-4 h-4')"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
211
app/templates/shared/macros/badges.html
Normal file
211
app/templates/shared/macros/badges.html
Normal file
@@ -0,0 +1,211 @@
|
||||
{#
|
||||
Badge Macros
|
||||
============
|
||||
Reusable badge components for status indicators, labels, and tags.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/badges.html' import badge, status_badge, verification_badge, role_badge %}
|
||||
{{ badge('New', 'purple') }}
|
||||
{{ status_badge('is_active', 'Active', 'Inactive') }}
|
||||
{{ verification_badge('item.is_verified') }}
|
||||
{{ role_badge('user.role') }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Basic Badge
|
||||
===========
|
||||
A simple colored badge.
|
||||
|
||||
Parameters:
|
||||
- text: Badge text (static)
|
||||
- color: 'gray' | 'red' | 'green' | 'blue' | 'yellow' | 'purple' | 'orange' | 'pink' (default: 'gray')
|
||||
- size: 'sm' | 'md' (default: 'sm')
|
||||
#}
|
||||
{% macro badge(text, color='gray', size='sm') %}
|
||||
{% set colors = {
|
||||
'gray': 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700',
|
||||
'red': 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
|
||||
'green': 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700',
|
||||
'blue': 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
|
||||
'yellow': 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700',
|
||||
'purple': 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700',
|
||||
'orange': 'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700',
|
||||
'pink': 'text-pink-700 bg-pink-100 dark:text-pink-100 dark:bg-pink-700'
|
||||
} %}
|
||||
{% set sizes = {
|
||||
'sm': 'px-2 py-1 text-xs',
|
||||
'md': 'px-3 py-1 text-sm'
|
||||
} %}
|
||||
<span class="inline-flex items-center font-semibold leading-tight rounded-full {{ colors[color] }} {{ sizes[size] }}">
|
||||
{{ text }}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Dynamic Badge
|
||||
=============
|
||||
A badge with text from Alpine.js variable.
|
||||
|
||||
Parameters:
|
||||
- text_var: Alpine.js variable for badge text
|
||||
- color: Badge color (default: 'gray')
|
||||
- size: 'sm' | 'md' (default: 'sm')
|
||||
#}
|
||||
{% macro badge_dynamic(text_var, color='gray', size='sm') %}
|
||||
{% set colors = {
|
||||
'gray': 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700',
|
||||
'red': 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
|
||||
'green': 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700',
|
||||
'blue': 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
|
||||
'yellow': 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700',
|
||||
'purple': 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700',
|
||||
'orange': 'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700',
|
||||
'pink': 'text-pink-700 bg-pink-100 dark:text-pink-100 dark:bg-pink-700'
|
||||
} %}
|
||||
{% set sizes = {
|
||||
'sm': 'px-2 py-1 text-xs',
|
||||
'md': 'px-3 py-1 text-sm'
|
||||
} %}
|
||||
<span class="inline-flex items-center font-semibold leading-tight rounded-full {{ colors[color] }} {{ sizes[size] }}" x-text="{{ text_var }}"></span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Status Badge (Boolean)
|
||||
======================
|
||||
A badge that changes based on a boolean condition.
|
||||
|
||||
Parameters:
|
||||
- condition: Alpine.js boolean expression
|
||||
- true_label: Text when true (default: 'Active')
|
||||
- false_label: Text when false (default: 'Inactive')
|
||||
- true_color: Color when true (default: 'green')
|
||||
- false_color: Color when false (default: 'gray')
|
||||
#}
|
||||
{% macro status_badge(condition, true_label='Active', false_label='Inactive', true_color='green', false_color='gray') %}
|
||||
{% set colors = {
|
||||
'gray': 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700',
|
||||
'red': 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700',
|
||||
'green': 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700',
|
||||
'blue': 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700',
|
||||
'yellow': 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700',
|
||||
'purple': 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700',
|
||||
'orange': 'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700'
|
||||
} %}
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="{{ condition }} ? '{{ colors[true_color] }}' : '{{ colors[false_color] }}'">
|
||||
<span x-text="{{ condition }} ? '{{ true_label }}' : '{{ false_label }}'"></span>
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Verification Badge
|
||||
==================
|
||||
A specialized badge for verified/pending status with icon.
|
||||
|
||||
Parameters:
|
||||
- condition: Alpine.js boolean expression (e.g., 'item.is_verified')
|
||||
- verified_label: Text when verified (default: 'Verified')
|
||||
- pending_label: Text when not verified (default: 'Pending')
|
||||
#}
|
||||
{% macro verification_badge(condition, verified_label='Verified', pending_label='Pending') %}
|
||||
<span class="inline-flex items-center px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="{{ condition }} ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
|
||||
<span x-show="{{ condition }}" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="{{ condition }} ? '{{ verified_label }}' : '{{ pending_label }}'"></span>
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Role Badge
|
||||
==========
|
||||
A badge for user roles with role-specific colors.
|
||||
|
||||
Parameters:
|
||||
- role_var: Alpine.js variable containing the role string
|
||||
- capitalize: Whether to capitalize the role text (default: true)
|
||||
#}
|
||||
{% macro role_badge(role_var, capitalize=true) %}
|
||||
<span class="px-2 py-1 text-xs font-semibold leading-tight rounded-full {{ 'capitalize' if capitalize else '' }}"
|
||||
:class="{
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': {{ role_var }} === 'admin',
|
||||
'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100': {{ role_var }} === 'vendor',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': {{ role_var }} === 'customer',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !['admin', 'vendor', 'customer'].includes({{ role_var }})
|
||||
}"
|
||||
x-text="{{ role_var }}">
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Order Status Badge
|
||||
==================
|
||||
A specialized badge for order statuses.
|
||||
|
||||
Parameters:
|
||||
- status_var: Alpine.js variable containing the status string
|
||||
#}
|
||||
{% macro order_status_badge(status_var) %}
|
||||
<span class="px-2 py-1 text-xs font-semibold leading-tight rounded-full capitalize"
|
||||
:class="{
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': {{ status_var }} === 'pending',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': {{ status_var }} === 'processing',
|
||||
'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100': {{ status_var }} === 'shipped',
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': {{ status_var }} === 'delivered',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': {{ status_var }} === 'cancelled',
|
||||
'text-gray-700 bg-gray-100 dark:bg-gray-700 dark:text-gray-100': !['pending', 'processing', 'shipped', 'delivered', 'cancelled'].includes({{ status_var }})
|
||||
}"
|
||||
x-text="{{ status_var }}">
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Severity Badge
|
||||
==============
|
||||
A badge for severity levels (error, warning, info).
|
||||
|
||||
Parameters:
|
||||
- severity_var: Alpine.js variable containing 'error' | 'warning' | 'info'
|
||||
#}
|
||||
{% macro severity_badge(severity_var) %}
|
||||
<span class="px-2 py-1 text-xs font-semibold leading-tight rounded-full"
|
||||
:class="{
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': {{ severity_var }} === 'error',
|
||||
'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700': {{ severity_var }} === 'warning',
|
||||
'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700': {{ severity_var }} === 'info'
|
||||
}"
|
||||
x-text="{{ severity_var }}">
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Count Badge
|
||||
===========
|
||||
A small badge typically used for counts (notifications, items, etc.).
|
||||
|
||||
Parameters:
|
||||
- count_var: Alpine.js variable or expression for the count
|
||||
- color: Badge color (default: 'red')
|
||||
- show_zero: Whether to show when count is 0 (default: false)
|
||||
#}
|
||||
{% macro count_badge(count_var, color='red', show_zero=false) %}
|
||||
{% set colors = {
|
||||
'red': 'bg-red-500 text-white',
|
||||
'green': 'bg-green-500 text-white',
|
||||
'blue': 'bg-blue-500 text-white',
|
||||
'purple': 'bg-purple-500 text-white',
|
||||
'gray': 'bg-gray-500 text-white'
|
||||
} %}
|
||||
<span
|
||||
x-show="{{ count_var }} > 0 || {{ 'true' if show_zero else 'false' }}"
|
||||
class="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none rounded-full {{ colors[color] }}"
|
||||
x-text="{{ count_var }}">
|
||||
</span>
|
||||
{% endmacro %}
|
||||
224
app/templates/shared/macros/buttons.html
Normal file
224
app/templates/shared/macros/buttons.html
Normal file
@@ -0,0 +1,224 @@
|
||||
{#
|
||||
Button Macros
|
||||
=============
|
||||
Reusable button components for actions, navigation, and forms.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/buttons.html' import btn, btn_primary, btn_secondary, btn_danger, action_button %}
|
||||
{{ btn_primary('Save Changes', icon='save', type='submit') }}
|
||||
{{ btn_secondary('Cancel', href='/back') }}
|
||||
{{ btn_danger('Delete', onclick='deleteItem()') }}
|
||||
{{ action_button('eye', 'viewItem()', 'blue', 'View details') }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Base Button
|
||||
===========
|
||||
A flexible button component that can be styled as primary, secondary, etc.
|
||||
|
||||
Parameters:
|
||||
- text: Button text
|
||||
- variant: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'ghost' (default: 'primary')
|
||||
- size: 'sm' | 'md' | 'lg' (default: 'md')
|
||||
- icon: Icon name (optional, uses $icon helper)
|
||||
- icon_position: 'left' | 'right' (default: 'left')
|
||||
- type: 'button' | 'submit' | 'reset' (default: 'button')
|
||||
- href: If provided, renders as <a> tag instead of <button>
|
||||
- onclick: JavaScript onclick handler
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- loading: Alpine.js expression for loading state
|
||||
- class_extra: Additional CSS classes
|
||||
#}
|
||||
{% macro btn(text, variant='primary', size='md', icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, class_extra='') %}
|
||||
{% set variants = {
|
||||
'primary': 'text-white bg-purple-600 border-transparent hover:bg-purple-700 focus:shadow-outline-purple',
|
||||
'secondary': 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700',
|
||||
'danger': 'text-white bg-red-600 border-transparent hover:bg-red-700 focus:shadow-outline-red',
|
||||
'success': 'text-white bg-green-600 border-transparent hover:bg-green-700 focus:shadow-outline-green',
|
||||
'warning': 'text-white bg-yellow-600 border-transparent hover:bg-yellow-700 focus:shadow-outline-yellow',
|
||||
'ghost': 'text-gray-600 dark:text-gray-400 bg-transparent border-transparent hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
} %}
|
||||
{% set sizes = {
|
||||
'sm': 'px-3 py-1 text-xs',
|
||||
'md': 'px-4 py-2 text-sm',
|
||||
'lg': 'px-6 py-3 text-base'
|
||||
} %}
|
||||
{% set base_classes = 'inline-flex items-center font-medium leading-5 transition-colors duration-150 border rounded-lg focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed' %}
|
||||
|
||||
{% if href %}
|
||||
<a href="{{ href }}" class="{{ base_classes }} {{ variants[variant] }} {{ sizes[size] }} {{ class_extra }}">
|
||||
{% if icon and icon_position == 'left' %}
|
||||
<span x-html="$icon('{{ icon }}', 'w-4 h-4 {{ 'mr-2' if text else '' }}')"></span>
|
||||
{% endif %}
|
||||
{{ text }}
|
||||
{% if icon and icon_position == 'right' %}
|
||||
<span x-html="$icon('{{ icon }}', 'w-4 h-4 {{ 'ml-2' if text else '' }}')"></span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
type="{{ type }}"
|
||||
{% if onclick %}@click="{{ onclick }}"{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="{{ base_classes }} {{ variants[variant] }} {{ sizes[size] }} {{ class_extra }}"
|
||||
>
|
||||
{% if loading %}
|
||||
<span x-show="{{ loading }}" x-html="$icon('spinner', 'w-4 h-4 {{ 'mr-2' if text else '' }}')"></span>
|
||||
{% endif %}
|
||||
{% if icon and icon_position == 'left' %}
|
||||
<span {% if loading %}x-show="!{{ loading }}"{% endif %} x-html="$icon('{{ icon }}', 'w-4 h-4 {{ 'mr-2' if text else '' }}')"></span>
|
||||
{% endif %}
|
||||
{{ text }}
|
||||
{% if icon and icon_position == 'right' %}
|
||||
<span {% if loading %}x-show="!{{ loading }}"{% endif %} x-html="$icon('{{ icon }}', 'w-4 h-4 {{ 'ml-2' if text else '' }}')"></span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{# Convenience macros for common button types #}
|
||||
|
||||
{% macro btn_primary(text, icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, size='md') %}
|
||||
{{ btn(text, 'primary', size, icon, icon_position, type, href, onclick, disabled, loading) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro btn_secondary(text, icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, size='md') %}
|
||||
{{ btn(text, 'secondary', size, icon, icon_position, type, href, onclick, disabled, loading) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro btn_danger(text, icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, size='md') %}
|
||||
{{ btn(text, 'danger', size, icon, icon_position, type, href, onclick, disabled, loading) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro btn_success(text, icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, size='md') %}
|
||||
{{ btn(text, 'success', size, icon, icon_position, type, href, onclick, disabled, loading) }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro btn_ghost(text, icon=none, icon_position='left', type='button', href=none, onclick=none, disabled=none, loading=none, size='md') %}
|
||||
{{ btn(text, 'ghost', size, icon, icon_position, type, href, onclick, disabled, loading) }}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Action Button (Icon Only)
|
||||
=========================
|
||||
A small icon button for table actions (view, edit, delete).
|
||||
|
||||
Parameters:
|
||||
- icon: Icon name
|
||||
- onclick: JavaScript onclick handler
|
||||
- color: 'blue' | 'purple' | 'red' | 'green' | 'gray' (default: 'gray')
|
||||
- title: Tooltip text
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- href: If provided, renders as <a> tag
|
||||
#}
|
||||
{% macro action_button(icon, onclick=none, color='gray', title='', disabled=none, href=none) %}
|
||||
{% set colors = {
|
||||
'blue': 'text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700',
|
||||
'purple': 'text-purple-600 hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700',
|
||||
'red': 'text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700',
|
||||
'green': 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-gray-700',
|
||||
'gray': 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700',
|
||||
'orange': 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-gray-700'
|
||||
} %}
|
||||
{% set base_classes = 'flex items-center justify-center p-2 rounded-lg focus:outline-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed' %}
|
||||
|
||||
{% if href %}
|
||||
<a href="{{ href }}" class="{{ base_classes }} {{ colors[color] }}" title="{{ title }}">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
@click="{{ onclick }}"
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="{{ base_classes }} {{ colors[color] }}"
|
||||
title="{{ title }}"
|
||||
>
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Action Button Group
|
||||
===================
|
||||
A group of action buttons (typically for table rows).
|
||||
|
||||
Usage:
|
||||
{{ action_button_group([
|
||||
{'icon': 'eye', 'onclick': 'view(item)', 'color': 'blue', 'title': 'View'},
|
||||
{'icon': 'edit', 'onclick': 'edit(item)', 'color': 'purple', 'title': 'Edit'},
|
||||
{'icon': 'delete', 'onclick': 'delete(item)', 'color': 'red', 'title': 'Delete'}
|
||||
]) }}
|
||||
#}
|
||||
{% macro action_button_group(buttons) %}
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
{% for button in buttons %}
|
||||
{{ action_button(
|
||||
button.icon,
|
||||
button.get('onclick'),
|
||||
button.get('color', 'gray'),
|
||||
button.get('title', ''),
|
||||
button.get('disabled'),
|
||||
button.get('href')
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Button Group
|
||||
============
|
||||
A group of buttons with proper spacing.
|
||||
#}
|
||||
{% macro button_group(class_extra='') %}
|
||||
<div class="flex items-center space-x-3 {{ class_extra }}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Back Button
|
||||
===========
|
||||
A standardized back navigation button.
|
||||
|
||||
Parameters:
|
||||
- href: URL to navigate back to
|
||||
- text: Button text (default: 'Back')
|
||||
#}
|
||||
{% macro back_button(href, text='Back') %}
|
||||
<a href="{{ href }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
{{ text }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Submit Button with Loading
|
||||
==========================
|
||||
A form submit button with built-in loading state.
|
||||
|
||||
Parameters:
|
||||
- text: Button text
|
||||
- loading_text: Text shown while loading (default: 'Saving...')
|
||||
- loading_var: Alpine.js variable for loading state (default: 'saving')
|
||||
- icon: Icon name (default: 'save')
|
||||
#}
|
||||
{% macro submit_button(text='Save', loading_text='Saving...', loading_var='saving', icon='save') %}
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="{{ loading_var }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="{{ loading_var }}" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="!{{ loading_var }}" x-html="$icon('{{ icon }}', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="{{ loading_var }} ? '{{ loading_text }}' : '{{ text }}'"></span>
|
||||
</button>
|
||||
{% endmacro %}
|
||||
206
app/templates/shared/macros/cards.html
Normal file
206
app/templates/shared/macros/cards.html
Normal file
@@ -0,0 +1,206 @@
|
||||
{#
|
||||
Card Macros
|
||||
===========
|
||||
Reusable card components including stats cards, info cards, and action cards.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/cards.html' import stat_card, info_card, card %}
|
||||
{{ stat_card('users', 'Total Users', 'stats.total', 'blue') }}
|
||||
{{ info_card('User Details', 'View and edit user information') }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Stat Card
|
||||
=========
|
||||
A statistics card with icon and value.
|
||||
|
||||
Parameters:
|
||||
- icon: Icon name
|
||||
- label: Card label/title
|
||||
- value: Alpine.js expression for the value
|
||||
- color: 'orange' | 'green' | 'blue' | 'purple' | 'red' | 'yellow' (default: 'orange')
|
||||
- format: Optional format function to apply to value
|
||||
#}
|
||||
{% macro stat_card(icon, label, value, color='orange', format=none) %}
|
||||
{% set colors = {
|
||||
'orange': 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500',
|
||||
'green': 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500',
|
||||
'blue': 'text-blue-500 bg-blue-100 dark:text-blue-100 dark:bg-blue-500',
|
||||
'purple': 'text-purple-500 bg-purple-100 dark:text-purple-100 dark:bg-purple-500',
|
||||
'red': 'text-red-500 bg-red-100 dark:text-red-100 dark:bg-red-500',
|
||||
'yellow': 'text-yellow-500 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-500',
|
||||
'teal': 'text-teal-500 bg-teal-100 dark:text-teal-100 dark:bg-teal-500'
|
||||
} %}
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 rounded-full {{ colors[color] }}">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ label }}
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200"
|
||||
x-text="{% if format %}{{ format }}({{ value }}){% else %}{{ value }} || 0{% endif %}">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Stats Grid
|
||||
==========
|
||||
A grid container for stat cards.
|
||||
|
||||
Parameters:
|
||||
- columns: Number of columns (default: 4)
|
||||
#}
|
||||
{% macro stats_grid(columns=4) %}
|
||||
{% set col_classes = {
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-3',
|
||||
4: 'md:grid-cols-2 xl:grid-cols-4'
|
||||
} %}
|
||||
<div class="grid gap-6 mb-8 {{ col_classes.get(columns, 'md:grid-cols-4') }}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Card
|
||||
====
|
||||
A basic card container.
|
||||
|
||||
Parameters:
|
||||
- title: Card title (optional)
|
||||
- subtitle: Card subtitle (optional)
|
||||
- class_extra: Additional CSS classes
|
||||
- padding: Whether to add padding (default: true)
|
||||
#}
|
||||
{% macro card(title=none, subtitle=none, class_extra='', padding=true) %}
|
||||
<div class="bg-white rounded-lg shadow-xs dark:bg-gray-800 {{ 'p-6' if padding else '' }} {{ class_extra }}">
|
||||
{% if title %}
|
||||
<div class="{% if padding %}{% else %}p-4 border-b dark:border-gray-700{% endif %}">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h3>
|
||||
{% if subtitle %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="{% if title and not padding %}p-4{% endif %}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Info Card
|
||||
=========
|
||||
An informational card with icon and description.
|
||||
|
||||
Parameters:
|
||||
- icon: Icon name
|
||||
- title: Card title
|
||||
- description: Card description
|
||||
- color: Icon color (default: 'purple')
|
||||
- href: Optional link URL
|
||||
#}
|
||||
{% macro info_card(icon, title, description, color='purple', href=none) %}
|
||||
{% set colors = {
|
||||
'purple': 'text-purple-500 bg-purple-100 dark:text-purple-100 dark:bg-purple-500',
|
||||
'blue': 'text-blue-500 bg-blue-100 dark:text-blue-100 dark:bg-blue-500',
|
||||
'green': 'text-green-500 bg-green-100 dark:text-green-100 dark:bg-green-500',
|
||||
'orange': 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'
|
||||
} %}
|
||||
{% if href %}
|
||||
<a href="{{ href }}" class="block p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 hover:shadow-md transition-shadow">
|
||||
{% else %}
|
||||
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
{% endif %}
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 mr-4 rounded-full {{ colors[color] }}">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if href %}
|
||||
</a>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Quick Actions Card
|
||||
==================
|
||||
A card with quick action buttons and status badges.
|
||||
|
||||
Parameters:
|
||||
- title: Card title (default: 'Quick Actions')
|
||||
#}
|
||||
{% macro quick_actions_card(title='Quick Actions') %}
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Coming Soon Card
|
||||
================
|
||||
A placeholder card for features under development.
|
||||
|
||||
Parameters:
|
||||
- emoji: Emoji to display
|
||||
- title: Feature title
|
||||
- description: Feature description
|
||||
- back_url: URL for back button
|
||||
- back_label: Back button text (default: 'Go Back')
|
||||
#}
|
||||
{% macro coming_soon_card(emoji='📦', title='Coming Soon', description='This feature is under development.', back_url='/', back_label='Go Back') %}
|
||||
<div class="w-full mb-8 overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full p-12 bg-white dark:bg-gray-800 text-center">
|
||||
<div class="text-6xl mb-4">{{ emoji }}</div>
|
||||
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||
{{ description }}
|
||||
</p>
|
||||
<a href="{{ back_url }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
{{ back_label }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Filter Card
|
||||
===========
|
||||
A card for search and filter controls.
|
||||
|
||||
Parameters:
|
||||
- show_condition: Alpine.js condition (default: '!loading')
|
||||
#}
|
||||
{% macro filter_card(show_condition='!loading') %}
|
||||
<div x-show="{{ show_condition }}" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
332
app/templates/shared/macros/forms.html
Normal file
332
app/templates/shared/macros/forms.html
Normal file
@@ -0,0 +1,332 @@
|
||||
{#
|
||||
Form Macros
|
||||
===========
|
||||
Reusable form input components.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/forms.html' import form_input, form_select, form_textarea, form_checkbox %}
|
||||
{{ form_input('Email', 'email', 'formData.email', type='email', required=true) }}
|
||||
{{ form_select('Status', 'formData.status', [{'value': 'active', 'label': 'Active'}]) }}
|
||||
{{ form_textarea('Description', 'formData.description', rows=4) }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Form Input
|
||||
==========
|
||||
A text input field with label, validation, and error handling.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- name: Input name attribute
|
||||
- x_model: Alpine.js x-model binding
|
||||
- type: Input type (default: 'text')
|
||||
- placeholder: Placeholder text
|
||||
- required: Whether the field is required (default: false)
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- error: Alpine.js expression for error message
|
||||
- help: Help text shown below the input
|
||||
- maxlength: Maximum length
|
||||
- min: Minimum value (for number inputs)
|
||||
- max: Maximum value (for number inputs)
|
||||
- step: Step value (for number inputs)
|
||||
- autocomplete: Autocomplete attribute
|
||||
- class_extra: Additional CSS classes for the input
|
||||
#}
|
||||
{% macro form_input(label, name, x_model, type='text', placeholder='', required=false, disabled=none, error=none, help=none, maxlength=none, min=none, max=none, step=none, autocomplete=none, class_extra='') %}
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
|
||||
</span>
|
||||
<input
|
||||
type="{{ type }}"
|
||||
name="{{ name }}"
|
||||
x-model="{{ x_model }}"
|
||||
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if required %}required{% endif %}
|
||||
{% if maxlength %}maxlength="{{ maxlength }}"{% endif %}
|
||||
{% if min is not none %}min="{{ min }}"{% endif %}
|
||||
{% if max is not none %}max="{{ max }}"{% endif %}
|
||||
{% if step %}step="{{ step }}"{% endif %}
|
||||
{% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input {{ class_extra }}"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
>
|
||||
{% if error %}
|
||||
<span x-show="{{ error }}" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="{{ error }}"></span>
|
||||
{% endif %}
|
||||
{% if help %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ help }}</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Select
|
||||
===========
|
||||
A select dropdown with label and error handling.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- options: List of options [{'value': '', 'label': ''}] or Alpine.js expression
|
||||
- name: Input name attribute
|
||||
- required: Whether the field is required (default: false)
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- error: Alpine.js expression for error message
|
||||
- placeholder: First empty option text (default: 'Select...')
|
||||
- on_change: Alpine.js @change handler
|
||||
#}
|
||||
{% macro form_select(label, x_model, options=[], name='', required=false, disabled=none, error=none, placeholder='Select...', on_change=none) %}
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
|
||||
</span>
|
||||
<select
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
x-model="{{ x_model }}"
|
||||
{% if required %}required{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
{% if on_change %}@change="{{ on_change }}"{% endif %}
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
>
|
||||
{% if placeholder %}
|
||||
<option value="">{{ placeholder }}</option>
|
||||
{% endif %}
|
||||
{% for option in options %}
|
||||
<option value="{{ option.value }}">{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if error %}
|
||||
<span x-show="{{ error }}" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="{{ error }}"></span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Select (Dynamic Options)
|
||||
=============================
|
||||
A select dropdown with options from Alpine.js.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- options_var: Alpine.js variable containing options array
|
||||
- value_key: Key for option value (default: 'value')
|
||||
- label_key: Key for option label (default: 'label')
|
||||
- ... (other params same as form_select)
|
||||
#}
|
||||
{% macro form_select_dynamic(label, x_model, options_var, value_key='value', label_key='label', name='', required=false, disabled=none, error=none, placeholder='Select...', on_change=none) %}
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
|
||||
</span>
|
||||
<select
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
x-model="{{ x_model }}"
|
||||
{% if required %}required{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
{% if on_change %}@change="{{ on_change }}"{% endif %}
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
>
|
||||
{% if placeholder %}
|
||||
<option value="">{{ placeholder }}</option>
|
||||
{% endif %}
|
||||
<template x-for="option in {{ options_var }}" :key="option.{{ value_key }}">
|
||||
<option :value="option.{{ value_key }}" x-text="option.{{ label_key }}"></option>
|
||||
</template>
|
||||
</select>
|
||||
{% if error %}
|
||||
<span x-show="{{ error }}" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="{{ error }}"></span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Textarea
|
||||
=============
|
||||
A textarea field with label and error handling.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- name: Input name attribute
|
||||
- rows: Number of rows (default: 3)
|
||||
- placeholder: Placeholder text
|
||||
- required: Whether the field is required (default: false)
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- error: Alpine.js expression for error message
|
||||
- maxlength: Maximum length
|
||||
#}
|
||||
{% macro form_textarea(label, x_model, name='', rows=3, placeholder='', required=false, disabled=none, error=none, maxlength=none) %}
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
|
||||
</span>
|
||||
<textarea
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
x-model="{{ x_model }}"
|
||||
rows="{{ rows }}"
|
||||
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if required %}required{% endif %}
|
||||
{% if maxlength %}maxlength="{{ maxlength }}"{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
></textarea>
|
||||
{% if error %}
|
||||
<span x-show="{{ error }}" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="{{ error }}"></span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Checkbox
|
||||
=============
|
||||
A checkbox input with label.
|
||||
|
||||
Parameters:
|
||||
- label: Checkbox label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- name: Input name attribute
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- help: Help text shown below
|
||||
#}
|
||||
{% macro form_checkbox(label, x_model, name='', disabled=none, help=none) %}
|
||||
<label class="flex items-center mb-4 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
x-model="{{ x_model }}"
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="form-checkbox h-4 w-4 text-purple-600 dark:bg-gray-700 dark:border-gray-600 focus:ring-purple-500 dark:focus:ring-purple-600"
|
||||
>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-400">{{ label }}</span>
|
||||
</label>
|
||||
{% if help %}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400 -mt-2 mb-4 block ml-6">{{ help }}</span>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Toggle Switch
|
||||
==================
|
||||
A toggle switch (styled checkbox).
|
||||
|
||||
Parameters:
|
||||
- label: Toggle label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
#}
|
||||
{% macro form_toggle(label, x_model, disabled=none) %}
|
||||
<div class="flex items-center mb-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ x_model }} = !{{ x_model }}"
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="{{ x_model }} ? 'bg-purple-600' : 'bg-gray-200 dark:bg-gray-700'"
|
||||
role="switch"
|
||||
:aria-checked="{{ x_model }}"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
:class="{{ x_model }} ? 'translate-x-5' : 'translate-x-0'"
|
||||
></span>
|
||||
</button>
|
||||
<span class="ml-3 text-sm text-gray-700 dark:text-gray-400">{{ label }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Form Radio Group
|
||||
================
|
||||
A group of radio buttons.
|
||||
|
||||
Parameters:
|
||||
- label: Group label
|
||||
- name: Radio group name
|
||||
- x_model: Alpine.js x-model binding
|
||||
- options: List of options [{'value': '', 'label': ''}]
|
||||
- inline: Whether to display inline (default: false)
|
||||
#}
|
||||
{% macro form_radio_group(label, name, x_model, options=[], inline=false) %}
|
||||
<div class="mb-4">
|
||||
<span class="block text-sm text-gray-700 dark:text-gray-400 mb-2">{{ label }}</span>
|
||||
<div class="{{ 'flex flex-wrap gap-4' if inline else 'space-y-2' }}">
|
||||
{% for option in options %}
|
||||
<label class="flex items-center text-sm">
|
||||
<input
|
||||
type="radio"
|
||||
name="{{ name }}"
|
||||
value="{{ option.value }}"
|
||||
x-model="{{ x_model }}"
|
||||
class="form-radio h-4 w-4 text-purple-600 dark:bg-gray-700 dark:border-gray-600 focus:ring-purple-500"
|
||||
>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-400">{{ option.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Search Input
|
||||
============
|
||||
A search input with icon.
|
||||
|
||||
Parameters:
|
||||
- x_model: Alpine.js x-model binding
|
||||
- placeholder: Placeholder text (default: 'Search...')
|
||||
- on_input: Alpine.js @input handler (e.g., 'debouncedSearch()')
|
||||
- class_extra: Additional CSS classes
|
||||
#}
|
||||
{% macro search_input(x_model, placeholder='Search...', on_input=none, class_extra='') %}
|
||||
<div class="relative {{ class_extra }}">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
x-model="{{ x_model }}"
|
||||
{% if on_input %}@input="{{ on_input }}"{% endif %}
|
||||
placeholder="{{ placeholder }}"
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Filter Select
|
||||
=============
|
||||
A compact select for filtering (no label, inline style).
|
||||
|
||||
Parameters:
|
||||
- x_model: Alpine.js x-model binding
|
||||
- options: List of options [{'value': '', 'label': ''}]
|
||||
- on_change: Alpine.js @change handler
|
||||
- placeholder: First option text (default: 'All')
|
||||
#}
|
||||
{% macro filter_select(x_model, options=[], on_change=none, placeholder='All') %}
|
||||
<select
|
||||
x-model="{{ x_model }}"
|
||||
{% if on_change %}@change="{{ on_change }}"{% endif %}
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">{{ placeholder }}</option>
|
||||
{% for option in options %}
|
||||
<option value="{{ option.value }}">{{ option.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endmacro %}
|
||||
222
app/templates/shared/macros/headers.html
Normal file
222
app/templates/shared/macros/headers.html
Normal file
@@ -0,0 +1,222 @@
|
||||
{#
|
||||
Header Macros
|
||||
=============
|
||||
Reusable page header and navigation components.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/headers.html' import page_header, section_header, breadcrumbs %}
|
||||
{{ page_header('User Management', action_label='Create User', action_url='/users/create') }}
|
||||
{{ section_header('Account Settings') }}
|
||||
{{ breadcrumbs([{'label': 'Home', 'url': '/'}, {'label': 'Users'}]) }}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Page Header
|
||||
===========
|
||||
A page header with title, optional subtitle, and action button.
|
||||
|
||||
Parameters:
|
||||
- title: Page title
|
||||
- subtitle: Page subtitle (optional)
|
||||
- action_label: Action button text (optional)
|
||||
- action_url: Action button URL (optional)
|
||||
- action_icon: Action button icon (default: 'plus')
|
||||
- action_onclick: JavaScript onclick handler (optional, instead of URL)
|
||||
- back_url: Back button URL (optional)
|
||||
- back_label: Back button text (default: 'Back')
|
||||
#}
|
||||
{% macro page_header(title, subtitle=none, action_label=none, action_url=none, action_icon='plus', action_onclick=none, back_url=none, back_label='Back') %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
{{ title }}
|
||||
</h2>
|
||||
{% if subtitle %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
{% if back_url %}
|
||||
<a href="{{ back_url }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
{{ back_label }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if action_label and (action_url or action_onclick) %}
|
||||
{% if action_url %}
|
||||
<a href="{{ action_url }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('{{ action_icon }}', 'w-4 h-4 mr-2')"></span>
|
||||
{{ action_label }}
|
||||
</a>
|
||||
{% else %}
|
||||
<button @click="{{ action_onclick }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('{{ action_icon }}', 'w-4 h-4 mr-2')"></span>
|
||||
{{ action_label }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Page Header (Dynamic)
|
||||
=====================
|
||||
A page header where the title comes from Alpine.js.
|
||||
|
||||
Parameters:
|
||||
- title_var: Alpine.js variable for the title
|
||||
- subtitle_var: Alpine.js variable for subtitle (optional)
|
||||
- ... other params same as page_header
|
||||
#}
|
||||
{% macro page_header_dynamic(title_var, subtitle_var=none, action_label=none, action_url=none, action_icon='plus', back_url=none, back_label='Back') %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="{{ title_var }}"></h2>
|
||||
{% if subtitle_var %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="{{ subtitle_var }}"></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
{% if back_url %}
|
||||
<a href="{{ back_url }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
{{ back_label }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if action_label and action_url %}
|
||||
<a href="{{ action_url }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('{{ action_icon }}', 'w-4 h-4 mr-2')"></span>
|
||||
{{ action_label }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Section Header
|
||||
==============
|
||||
A section header within a page.
|
||||
|
||||
Parameters:
|
||||
- title: Section title
|
||||
- subtitle: Section subtitle (optional)
|
||||
- action_label: Action button text (optional)
|
||||
- action_onclick: Action button onclick (optional)
|
||||
- icon: Section icon (optional)
|
||||
#}
|
||||
{% macro section_header(title, subtitle=none, action_label=none, action_onclick=none, icon=none) %}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
{% if icon %}
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5 mr-2 text-gray-500')"></span>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h3>
|
||||
{% if subtitle %}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if action_label and action_onclick %}
|
||||
<button @click="{{ action_onclick }}"
|
||||
class="px-3 py-1 text-sm font-medium text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
||||
{{ action_label }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Breadcrumbs
|
||||
===========
|
||||
Navigation breadcrumbs.
|
||||
|
||||
Parameters:
|
||||
- items: List of breadcrumb items [{'label': '', 'url': ''}]
|
||||
Last item (without url) is treated as current page
|
||||
- separator: Separator character (default: '/')
|
||||
#}
|
||||
{% macro breadcrumbs(items, separator='/') %}
|
||||
<nav class="flex mb-6" aria-label="Breadcrumb">
|
||||
<ol class="inline-flex items-center space-x-1 md:space-x-2 text-sm">
|
||||
{% for item in items %}
|
||||
<li class="inline-flex items-center">
|
||||
{% if not loop.first %}
|
||||
<span class="mx-2 text-gray-400">{{ separator }}</span>
|
||||
{% endif %}
|
||||
{% if item.url and not loop.last %}
|
||||
<a href="{{ item.url }}" class="text-gray-600 dark:text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-900 dark:text-gray-200 font-medium">{{ item.label }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Card Header
|
||||
===========
|
||||
A header for card sections.
|
||||
|
||||
Parameters:
|
||||
- title: Card header title
|
||||
- subtitle: Card header subtitle (optional)
|
||||
- class_extra: Additional CSS classes
|
||||
#}
|
||||
{% macro card_header(title, subtitle=none, class_extra='') %}
|
||||
<div class="mb-4 {{ class_extra }}">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h3>
|
||||
{% if subtitle %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Tab Header
|
||||
==========
|
||||
A tab navigation header.
|
||||
|
||||
Parameters:
|
||||
- tabs: List of tab items [{'id': '', 'label': '', 'icon': ''}]
|
||||
- active_var: Alpine.js variable for active tab
|
||||
#}
|
||||
{% macro tab_header(tabs, active_var='activeTab') %}
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<nav class="flex space-x-4" aria-label="Tabs">
|
||||
{% for tab in tabs %}
|
||||
<button
|
||||
@click="{{ active_var }} = '{{ tab.id }}'"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors"
|
||||
:class="{{ active_var }} === '{{ tab.id }}'
|
||||
? 'border-purple-600 text-purple-600 dark:text-purple-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'"
|
||||
>
|
||||
{% if tab.icon %}
|
||||
<span x-html="$icon('{{ tab.icon }}', 'w-4 h-4 inline mr-2')"></span>
|
||||
{% endif %}
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
128
app/templates/shared/macros/pagination.html
Normal file
128
app/templates/shared/macros/pagination.html
Normal file
@@ -0,0 +1,128 @@
|
||||
{#
|
||||
Pagination Macro
|
||||
================
|
||||
A reusable pagination component for tables and lists.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/pagination.html' import pagination %}
|
||||
{{ pagination() }}
|
||||
|
||||
Required Alpine.js data properties:
|
||||
- pagination.page: Current page number
|
||||
- pagination.total: Total number of items
|
||||
- startIndex: First item index on current page
|
||||
- endIndex: Last item index on current page
|
||||
- totalPages: Total number of pages
|
||||
- pageNumbers: Array of page numbers with '...' for ellipsis
|
||||
|
||||
Required Alpine.js methods:
|
||||
- previousPage(): Go to previous page
|
||||
- nextPage(): Go to next page
|
||||
- goToPage(pageNum): Go to specific page
|
||||
#}
|
||||
|
||||
{% macro pagination(show_condition="true") %}
|
||||
<div x-show="{{ show_condition }}" class="grid px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
|
||||
{# Results Info #}
|
||||
<span class="flex items-center col-span-3">
|
||||
Showing <span class="mx-1 font-bold" x-text="startIndex"></span>-<span class="mx-1 font-bold" x-text="endIndex"></span> of <span class="mx-1 font-bold" x-text="pagination.total"></span>
|
||||
</span>
|
||||
<span class="col-span-2"></span>
|
||||
|
||||
{# Pagination Controls #}
|
||||
<span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
|
||||
<nav aria-label="Table navigation">
|
||||
<ul class="inline-flex items-center">
|
||||
{# Previous Button #}
|
||||
<li>
|
||||
<button
|
||||
@click="previousPage()"
|
||||
:disabled="pagination.page === 1"
|
||||
class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple"
|
||||
:class="pagination.page === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<svg class="w-4 h-4 fill-current" aria-hidden="true" viewBox="0 0 20 20">
|
||||
<path d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{# Page Numbers #}
|
||||
<template x-for="pageNum in pageNumbers" :key="pageNum">
|
||||
<li>
|
||||
<button
|
||||
x-show="pageNum !== '...'"
|
||||
@click="goToPage(pageNum)"
|
||||
class="px-3 py-1 rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||
:class="pagination.page === pageNum ? 'text-white bg-purple-600 border border-purple-600' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
x-text="pageNum"
|
||||
></button>
|
||||
<span x-show="pageNum === '...'" class="px-3 py-1">...</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
{# Next Button #}
|
||||
<li>
|
||||
<button
|
||||
@click="nextPage()"
|
||||
:disabled="pagination.page === totalPages"
|
||||
class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple"
|
||||
:class="pagination.page === totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
aria-label="Next"
|
||||
>
|
||||
<svg class="w-4 h-4 fill-current" aria-hidden="true" viewBox="0 0 20 20">
|
||||
<path d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Simple Pagination Macro (Prev/Next only)
|
||||
========================================
|
||||
A simpler pagination with just previous/next buttons.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/pagination.html' import pagination_simple %}
|
||||
{{ pagination_simple() }}
|
||||
#}
|
||||
|
||||
{% macro pagination_simple(show_condition="true") %}
|
||||
<div x-show="{{ show_condition }}" class="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-400">
|
||||
Showing <span class="font-semibold" x-text="startIndex"></span>-<span class="font-semibold" x-text="endIndex"></span>
|
||||
of <span class="font-semibold" x-text="pagination.total"></span> results
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="previousPage()"
|
||||
:disabled="pagination.page === 1"
|
||||
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-400">
|
||||
Page <span x-text="pagination.page"></span> of <span x-text="totalPages"></span>
|
||||
</span>
|
||||
<button
|
||||
@click="nextPage()"
|
||||
:disabled="pagination.page === totalPages"
|
||||
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
215
app/templates/shared/macros/tables.html
Normal file
215
app/templates/shared/macros/tables.html
Normal file
@@ -0,0 +1,215 @@
|
||||
{#
|
||||
Table Macros
|
||||
============
|
||||
Reusable table components.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_empty_state %}
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Name', 'Email', 'Status', 'Actions']) }}
|
||||
<tbody>...</tbody>
|
||||
{% endcall %}
|
||||
#}
|
||||
|
||||
|
||||
{#
|
||||
Table Wrapper
|
||||
=============
|
||||
Wraps the table with proper overflow and shadow styling.
|
||||
#}
|
||||
{% macro table_wrapper(class_extra='') %}
|
||||
<div class="w-full overflow-hidden rounded-lg shadow-xs {{ class_extra }}">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
{{ caller() }}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Header
|
||||
============
|
||||
Renders the table header row.
|
||||
|
||||
Parameters:
|
||||
- columns: List of column names
|
||||
- sortable: Whether columns are sortable (default: false) - future enhancement
|
||||
#}
|
||||
{% macro table_header(columns) %}
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
{% for column in columns %}
|
||||
<th class="px-4 py-3">{{ column }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Body Wrapper
|
||||
==================
|
||||
Wraps the tbody with proper styling.
|
||||
#}
|
||||
{% macro table_body() %}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
{{ caller() }}
|
||||
</tbody>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Empty State
|
||||
=================
|
||||
Shows a centered message when the table has no data.
|
||||
|
||||
Parameters:
|
||||
- colspan: Number of columns to span
|
||||
- icon: Icon name (default: 'inbox')
|
||||
- title: Empty state title
|
||||
- message: Empty state message
|
||||
- show_condition: Alpine.js condition (default: 'true')
|
||||
- has_filters: Whether to show filter hint (default: true)
|
||||
#}
|
||||
{% macro table_empty_state(colspan, icon='inbox', title='No data found', message='', show_condition='true', has_filters=true) %}
|
||||
<template x-if="{{ show_condition }}">
|
||||
<tr>
|
||||
<td colspan="{{ colspan }}" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('{{ icon }}', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p class="font-medium">{{ title }}</p>
|
||||
{% if message %}
|
||||
<p class="text-xs mt-1">{{ message }}</p>
|
||||
{% elif has_filters %}
|
||||
<p class="text-xs mt-1">Try adjusting your search or filters</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Row
|
||||
=========
|
||||
A standard table row with hover styling.
|
||||
#}
|
||||
{% macro table_row(class_extra='') %}
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 {{ class_extra }}">
|
||||
{{ caller() }}
|
||||
</tr>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Cell
|
||||
==========
|
||||
A standard table cell.
|
||||
#}
|
||||
{% macro table_cell(class_extra='') %}
|
||||
<td class="px-4 py-3 {{ class_extra }}">
|
||||
{{ caller() }}
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Cell with Text
|
||||
====================
|
||||
A simple text cell.
|
||||
|
||||
Parameters:
|
||||
- text: Static text or Alpine.js expression for x-text
|
||||
- is_dynamic: Whether text is Alpine.js expression (default: false)
|
||||
- truncate: Whether to truncate with max-width (default: false)
|
||||
- max_width: Max width class (default: 'max-w-xs')
|
||||
#}
|
||||
{% macro table_cell_text(text, is_dynamic=false, truncate=false, max_width='max-w-xs') %}
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{% if truncate %}
|
||||
<p class="truncate {{ max_width }}" {% if is_dynamic %}x-text="{{ text }}" :title="{{ text }}"{% endif %}>
|
||||
{% if not is_dynamic %}{{ text }}{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
{% if is_dynamic %}
|
||||
<span x-text="{{ text }}"></span>
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Cell with Avatar
|
||||
======================
|
||||
A cell with avatar image and text.
|
||||
|
||||
Parameters:
|
||||
- image_src: Image source (Alpine.js expression)
|
||||
- title: Primary text (Alpine.js expression)
|
||||
- subtitle: Secondary text (Alpine.js expression, optional)
|
||||
- fallback_icon: Icon to show if no image (default: 'user')
|
||||
#}
|
||||
{% macro table_cell_avatar(image_src, title, subtitle=none, fallback_icon='user') %}
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<template x-if="{{ image_src }}">
|
||||
<img class="object-cover w-full h-full rounded-full" :src="{{ image_src }}" :alt="{{ title }}" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!{{ image_src }}">
|
||||
<div class="flex items-center justify-center w-full h-full bg-gray-200 dark:bg-gray-700 rounded-full">
|
||||
<span x-html="$icon('{{ fallback_icon }}', 'w-4 h-4 text-gray-500')"></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="absolute inset-0 rounded-full shadow-inner" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold" x-text="{{ title }}"></p>
|
||||
{% if subtitle %}
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" x-text="{{ subtitle }}"></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Cell with Date
|
||||
====================
|
||||
A cell that formats a date.
|
||||
|
||||
Parameters:
|
||||
- date_var: Alpine.js variable containing the date
|
||||
- format_func: JavaScript date formatting function (default: 'formatDate')
|
||||
#}
|
||||
{% macro table_cell_date(date_var, format_func='formatDate') %}
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span x-text="{{ format_func }}({{ date_var }})"></span>
|
||||
</td>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Table Loading Overlay
|
||||
=====================
|
||||
An overlay shown while table data is loading.
|
||||
|
||||
Parameters:
|
||||
- show_condition: Alpine.js condition (default: 'loading')
|
||||
- message: Loading message
|
||||
#}
|
||||
{% macro table_loading_overlay(show_condition='loading', message='Loading...') %}
|
||||
<div x-show="{{ show_condition }}" class="absolute inset-0 bg-white/75 dark:bg-gray-800/75 flex items-center justify-center z-10">
|
||||
<div class="text-center">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user