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:
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 %}
|
||||
Reference in New Issue
Block a user