feat: add advanced UI component macros
Add new macro files for comprehensive UI coverage: - modals.html: modal, confirm_modal, form_modal, slide_over - dropdowns.html: dropdown, context_menu, dropdown_item, select_dropdown - avatars.html: avatar, avatar_with_status, avatar_initials, avatar_group, user_avatar_card - charts.html: chart_card, line_chart, bar_chart, doughnut_chart (Chart.js) - datepicker.html: datepicker, daterange_picker, datetime_picker, time_picker (Flatpickr) Update forms.html with: - password_input: Password field with show/hide toggle and strength indicator - input_with_icon: Input with left/right icon support - file_input: Drag & drop file upload zone Tech stack: Jinja2 + Alpine.js + Tailwind CSS External libs: Chart.js (optional), Flatpickr (optional) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,12 @@
|
||||
Reusable form input components.
|
||||
|
||||
Usage:
|
||||
{% from 'shared/macros/forms.html' import form_input, form_select, form_textarea, form_checkbox %}
|
||||
{% from 'shared/macros/forms.html' import form_input, form_select, form_textarea, form_checkbox, password_input, input_with_icon %}
|
||||
{{ 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) }}
|
||||
{{ password_input('Password', 'formData.password', required=true) }}
|
||||
{{ input_with_icon('Search', 'query', 'search', icon='search', icon_position='left') }}
|
||||
#}
|
||||
|
||||
|
||||
@@ -330,3 +332,215 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Password Input
|
||||
==============
|
||||
A password input with show/hide toggle.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- name: Input name attribute
|
||||
- placeholder: Placeholder text (default: 'Enter password')
|
||||
- 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
|
||||
- minlength: Minimum password length
|
||||
- autocomplete: Autocomplete attribute (default: 'current-password')
|
||||
- show_strength: Whether to show password strength indicator (default: false)
|
||||
#}
|
||||
{% macro password_input(label, x_model, name='password', placeholder='Enter password', required=false, disabled=none, error=none, help=none, minlength=none, autocomplete='current-password', show_strength=false) %}
|
||||
<div x-data="{ showPassword: false }" class="mb-4">
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
{{ label }}{% if required %} <span class="text-red-600">*</span>{% endif %}
|
||||
</span>
|
||||
<div class="relative mt-1">
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
name="{{ name }}"
|
||||
x-model="{{ x_model }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
{% if required %}required{% endif %}
|
||||
{% if minlength %}minlength="{{ minlength }}"{% endif %}
|
||||
autocomplete="{{ autocomplete }}"
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="block w-full pr-10 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"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none"
|
||||
>
|
||||
<template x-if="!showPassword">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="showPassword">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
|
||||
</svg>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
{% 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>
|
||||
{% if show_strength %}
|
||||
<div class="mt-2">
|
||||
<div class="flex gap-1">
|
||||
<div class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700"
|
||||
:class="{
|
||||
'bg-red-500': {{ x_model }}.length > 0 && {{ x_model }}.length < 6,
|
||||
'bg-yellow-500': {{ x_model }}.length >= 6 && {{ x_model }}.length < 10,
|
||||
'bg-green-500': {{ x_model }}.length >= 10
|
||||
}"></div>
|
||||
<div class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700"
|
||||
:class="{
|
||||
'bg-yellow-500': {{ x_model }}.length >= 6 && {{ x_model }}.length < 10,
|
||||
'bg-green-500': {{ x_model }}.length >= 10
|
||||
}"></div>
|
||||
<div class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700"
|
||||
:class="{ 'bg-green-500': {{ x_model }}.length >= 10 }"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1"
|
||||
x-text="{{ x_model }}.length < 6 ? 'Weak' : ({{ x_model }}.length < 10 ? 'Medium' : 'Strong')"></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Input with Icon
|
||||
===============
|
||||
A text input with an icon on the left or right.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- x_model: Alpine.js x-model binding
|
||||
- name: Input name attribute
|
||||
- icon: Icon name (uses $icon helper)
|
||||
- icon_position: 'left' | 'right' (default: 'left')
|
||||
- type: Input type (default: 'text')
|
||||
- placeholder: Placeholder text
|
||||
- required: Whether the field is required
|
||||
- disabled: Alpine.js expression for disabled state
|
||||
- error: Alpine.js expression for error message
|
||||
- on_click_icon: Alpine.js handler when icon is clicked (makes icon a button)
|
||||
#}
|
||||
{% macro input_with_icon(label, x_model, name, icon, icon_position='left', type='text', placeholder='', required=false, disabled=none, error=none, on_click_icon=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>
|
||||
<div class="relative mt-1">
|
||||
{% if icon_position == 'left' %}
|
||||
{% if on_click_icon %}
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ on_click_icon }}"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none"
|
||||
>
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5 text-gray-400')"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<input
|
||||
type="{{ type }}"
|
||||
name="{{ name }}"
|
||||
x-model="{{ x_model }}"
|
||||
{% if placeholder %}placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if required %}required{% endif %}
|
||||
{% if disabled %}:disabled="{{ disabled }}"{% endif %}
|
||||
class="block w-full 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 {{ 'pl-10' if icon_position == 'left' else 'pr-10' }}"
|
||||
{% if error %}:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': {{ error }} }"{% endif %}
|
||||
>
|
||||
{% if icon_position == 'right' %}
|
||||
{% if on_click_icon %}
|
||||
<button
|
||||
type="button"
|
||||
@click="{{ on_click_icon }}"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none"
|
||||
>
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span x-html="$icon('{{ icon }}', 'w-5 h-5 text-gray-400')"></span>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
|
||||
{#
|
||||
File Input
|
||||
==========
|
||||
A styled file input with drag and drop support.
|
||||
|
||||
Parameters:
|
||||
- label: Field label
|
||||
- name: Input name attribute
|
||||
- accept: Accepted file types (e.g., 'image/*', '.pdf,.doc')
|
||||
- multiple: Allow multiple files (default: false)
|
||||
- max_size: Maximum file size in MB (for display only)
|
||||
- on_change: Alpine.js handler when files are selected
|
||||
- help: Help text
|
||||
#}
|
||||
{% macro file_input(label, name, accept='*', multiple=false, max_size=none, on_change=none, help=none) %}
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1.5">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div
|
||||
x-data="{ isDragging: false, files: [] }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="isDragging = false; files = $event.dataTransfer.files; {% if on_change %}{{ on_change }}{% endif %}"
|
||||
class="relative border-2 border-dashed rounded-lg p-6 text-center transition-colors"
|
||||
:class="isDragging ? 'border-purple-500 bg-purple-50 dark:bg-purple-900/10' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="{{ name }}"
|
||||
accept="{{ accept }}"
|
||||
{% if multiple %}multiple{% endif %}
|
||||
@change="files = $event.target.files; {% if on_change %}{{ on_change }}{% endif %}"
|
||||
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<svg class="mx-auto w-10 h-10 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium text-purple-600 dark:text-purple-400">Click to upload</span> or drag and drop
|
||||
</p>
|
||||
{% if help %}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ help }}</p>
|
||||
{% elif max_size %}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Max file size: {{ max_size }}MB</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
Reference in New Issue
Block a user