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>
374 lines
12 KiB
HTML
374 lines
12 KiB
HTML
{#
|
|
Avatar Macros
|
|
=============
|
|
Reusable avatar components for user profile images.
|
|
|
|
Usage:
|
|
{% from 'shared/macros/avatars.html' import avatar, avatar_with_status, avatar_group, avatar_initials %}
|
|
|
|
{# Basic avatar with image #}
|
|
{{ avatar(src='user.avatar_url', alt='user.name', size='md') }}
|
|
|
|
{# Avatar with online status #}
|
|
{{ avatar_with_status(src='user.avatar', status='online', size='lg') }}
|
|
|
|
{# Avatar group (stacked) #}
|
|
{% call avatar_group(max=3) %}
|
|
{{ avatar(src='url1', size='sm') }}
|
|
{{ avatar(src='url2', size='sm') }}
|
|
{{ avatar(src='url3', size='sm') }}
|
|
{% endcall %}
|
|
|
|
{# Initials fallback #}
|
|
{{ avatar_initials(initials='JD', size='md', color='purple') }}
|
|
#}
|
|
|
|
|
|
{#
|
|
Avatar
|
|
======
|
|
A basic avatar component.
|
|
|
|
Parameters:
|
|
- src: Image source (static string or Alpine.js expression with :src)
|
|
- alt: Alt text
|
|
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' (default: 'md')
|
|
- rounded: 'full' | 'lg' | 'md' (default: 'full')
|
|
- fallback_icon: Icon to show if no image (default: 'user')
|
|
- dynamic: Whether src is an Alpine.js expression (default: false)
|
|
- class_extra: Additional CSS classes
|
|
#}
|
|
{% macro avatar(src='', alt='', size='md', rounded='full', fallback_icon='user', dynamic=false, class_extra='') %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6',
|
|
'sm': 'w-8 h-8',
|
|
'md': 'w-10 h-10',
|
|
'lg': 'w-12 h-12',
|
|
'xl': 'w-14 h-14',
|
|
'2xl': 'w-16 h-16'
|
|
} %}
|
|
{% set icon_sizes = {
|
|
'xs': 'w-3 h-3',
|
|
'sm': 'w-4 h-4',
|
|
'md': 'w-5 h-5',
|
|
'lg': 'w-6 h-6',
|
|
'xl': 'w-7 h-7',
|
|
'2xl': 'w-8 h-8'
|
|
} %}
|
|
{% set roundeds = {
|
|
'full': 'rounded-full',
|
|
'lg': 'rounded-lg',
|
|
'md': 'rounded-md'
|
|
} %}
|
|
<div class="relative inline-flex items-center justify-center {{ sizes[size] }} {{ roundeds[rounded] }} bg-gray-200 dark:bg-gray-700 overflow-hidden {{ class_extra }}">
|
|
{% if dynamic %}
|
|
<template x-if="{{ src }}">
|
|
<img :src="{{ src }}" :alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
</template>
|
|
<template x-if="!{{ src }}">
|
|
<span x-html="$icon('{{ fallback_icon }}', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
</template>
|
|
{% elif src %}
|
|
<img src="{{ src }}" alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
{% else %}
|
|
<span x-html="$icon('{{ fallback_icon }}', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Avatar with Status
|
|
==================
|
|
An avatar with an online/offline status indicator.
|
|
|
|
Parameters:
|
|
- src: Image source
|
|
- status: 'online' | 'offline' | 'away' | 'busy' | Alpine.js expression
|
|
- size: Avatar size (default: 'md')
|
|
- alt: Alt text
|
|
- dynamic: Whether values are Alpine.js expressions (default: false)
|
|
- show_status: Whether to show status indicator (default: true)
|
|
#}
|
|
{% macro avatar_with_status(src='', status='online', size='md', alt='', dynamic=false, show_status=true) %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6',
|
|
'sm': 'w-8 h-8',
|
|
'md': 'w-10 h-10',
|
|
'lg': 'w-12 h-12',
|
|
'xl': 'w-14 h-14',
|
|
'2xl': 'w-16 h-16'
|
|
} %}
|
|
{% set icon_sizes = {
|
|
'xs': 'w-3 h-3',
|
|
'sm': 'w-4 h-4',
|
|
'md': 'w-5 h-5',
|
|
'lg': 'w-6 h-6',
|
|
'xl': 'w-7 h-7',
|
|
'2xl': 'w-8 h-8'
|
|
} %}
|
|
{% set indicator_sizes = {
|
|
'xs': 'w-1.5 h-1.5',
|
|
'sm': 'w-2 h-2',
|
|
'md': 'w-2.5 h-2.5',
|
|
'lg': 'w-3 h-3',
|
|
'xl': 'w-3.5 h-3.5',
|
|
'2xl': 'w-4 h-4'
|
|
} %}
|
|
{% set indicator_positions = {
|
|
'xs': 'bottom-0 right-0',
|
|
'sm': 'bottom-0 right-0',
|
|
'md': 'bottom-0 right-0',
|
|
'lg': 'bottom-0.5 right-0.5',
|
|
'xl': 'bottom-0.5 right-0.5',
|
|
'2xl': 'bottom-1 right-1'
|
|
} %}
|
|
{% set status_colors = {
|
|
'online': 'bg-green-500',
|
|
'offline': 'bg-gray-400',
|
|
'away': 'bg-yellow-500',
|
|
'busy': 'bg-red-500'
|
|
} %}
|
|
<div class="relative inline-block">
|
|
<div class="relative inline-flex items-center justify-center {{ sizes[size] }} rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
|
|
{% if dynamic %}
|
|
<template x-if="{{ src }}">
|
|
<img :src="{{ src }}" :alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
</template>
|
|
<template x-if="!{{ src }}">
|
|
<span x-html="$icon('user', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
</template>
|
|
{% elif src %}
|
|
<img src="{{ src }}" alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
{% else %}
|
|
<span x-html="$icon('user', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
{% endif %}
|
|
</div>
|
|
{% if show_status %}
|
|
<span
|
|
class="absolute {{ indicator_positions[size] }} block {{ indicator_sizes[size] }} rounded-full ring-2 ring-white dark:ring-gray-800"
|
|
{% if dynamic %}
|
|
:class="{
|
|
'bg-green-500': {{ status }} === 'online',
|
|
'bg-gray-400': {{ status }} === 'offline',
|
|
'bg-yellow-500': {{ status }} === 'away',
|
|
'bg-red-500': {{ status }} === 'busy'
|
|
}"
|
|
{% else %}
|
|
class="{{ status_colors.get(status, 'bg-gray-400') }}"
|
|
{% endif %}
|
|
></span>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Avatar Initials
|
|
===============
|
|
An avatar showing initials instead of an image.
|
|
|
|
Parameters:
|
|
- initials: 1-2 character initials (static or Alpine.js expression)
|
|
- size: Avatar size (default: 'md')
|
|
- color: 'gray' | 'purple' | 'blue' | 'green' | 'red' | 'yellow' | 'orange' (default: 'purple')
|
|
- dynamic: Whether initials is an Alpine.js expression (default: false)
|
|
#}
|
|
{% macro avatar_initials(initials='', size='md', color='purple', dynamic=false) %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6 text-xs',
|
|
'sm': 'w-8 h-8 text-xs',
|
|
'md': 'w-10 h-10 text-sm',
|
|
'lg': 'w-12 h-12 text-base',
|
|
'xl': 'w-14 h-14 text-lg',
|
|
'2xl': 'w-16 h-16 text-xl'
|
|
} %}
|
|
{% set colors = {
|
|
'gray': 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300',
|
|
'purple': 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
|
|
'blue': 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
|
|
'green': 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
|
|
'red': 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400',
|
|
'yellow': 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400',
|
|
'orange': 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400'
|
|
} %}
|
|
<div class="inline-flex items-center justify-center rounded-full font-semibold {{ sizes[size] }} {{ colors[color] }}">
|
|
{% if dynamic %}
|
|
<span x-text="{{ initials }}"></span>
|
|
{% else %}
|
|
{{ initials }}
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Avatar with Fallback
|
|
====================
|
|
An avatar that shows initials when no image is available.
|
|
|
|
Parameters:
|
|
- src: Image source (Alpine.js expression)
|
|
- initials: Fallback initials (Alpine.js expression)
|
|
- size: Avatar size
|
|
- color: Initials background color
|
|
#}
|
|
{% macro avatar_with_fallback(src, initials, size='md', color='purple') %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6 text-xs',
|
|
'sm': 'w-8 h-8 text-xs',
|
|
'md': 'w-10 h-10 text-sm',
|
|
'lg': 'w-12 h-12 text-base',
|
|
'xl': 'w-14 h-14 text-lg',
|
|
'2xl': 'w-16 h-16 text-xl'
|
|
} %}
|
|
{% set colors = {
|
|
'gray': 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300',
|
|
'purple': 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
|
|
'blue': 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400',
|
|
'green': 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
|
|
'red': 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400'
|
|
} %}
|
|
<div class="inline-flex items-center justify-center rounded-full overflow-hidden {{ sizes[size] }} {{ colors[color] }}">
|
|
<template x-if="{{ src }}">
|
|
<img :src="{{ src }}" class="w-full h-full object-cover" loading="lazy">
|
|
</template>
|
|
<template x-if="!{{ src }}">
|
|
<span class="font-semibold" x-text="{{ initials }}"></span>
|
|
</template>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Avatar Group
|
|
============
|
|
A stacked group of avatars.
|
|
|
|
Parameters:
|
|
- max: Maximum number of avatars to show (default: 4)
|
|
- total_var: Alpine.js variable for total count (optional, for +N indicator)
|
|
- size: Avatar size (default: 'sm')
|
|
#}
|
|
{% macro avatar_group(max=4, total_var=none, size='sm') %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6',
|
|
'sm': 'w-8 h-8',
|
|
'md': 'w-10 h-10',
|
|
'lg': 'w-12 h-12'
|
|
} %}
|
|
{% set overlaps = {
|
|
'xs': '-space-x-2',
|
|
'sm': '-space-x-2',
|
|
'md': '-space-x-3',
|
|
'lg': '-space-x-4'
|
|
} %}
|
|
<div class="flex {{ overlaps[size] }}">
|
|
{{ caller() }}
|
|
{% if total_var %}
|
|
<div
|
|
x-show="{{ total_var }} > {{ max }}"
|
|
class="inline-flex items-center justify-center {{ sizes[size] }} rounded-full bg-gray-200 dark:bg-gray-700 text-xs font-medium text-gray-600 dark:text-gray-300 ring-2 ring-white dark:ring-gray-800"
|
|
>
|
|
<span x-text="'+' + ({{ total_var }} - {{ max }})"></span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
Avatar Group Item
|
|
=================
|
|
An avatar item for use within avatar_group.
|
|
Adds the ring styling for proper stacking.
|
|
|
|
Parameters:
|
|
- src: Image source
|
|
- alt: Alt text
|
|
- size: Avatar size (default: 'sm')
|
|
#}
|
|
{% macro avatar_group_item(src='', alt='', size='sm', dynamic=false) %}
|
|
{% set sizes = {
|
|
'xs': 'w-6 h-6',
|
|
'sm': 'w-8 h-8',
|
|
'md': 'w-10 h-10',
|
|
'lg': 'w-12 h-12'
|
|
} %}
|
|
{% set icon_sizes = {
|
|
'xs': 'w-3 h-3',
|
|
'sm': 'w-4 h-4',
|
|
'md': 'w-5 h-5',
|
|
'lg': 'w-6 h-6'
|
|
} %}
|
|
<div class="relative inline-flex items-center justify-center {{ sizes[size] }} rounded-full bg-gray-200 dark:bg-gray-700 ring-2 ring-white dark:ring-gray-800 overflow-hidden">
|
|
{% if dynamic %}
|
|
<template x-if="{{ src }}">
|
|
<img :src="{{ src }}" :alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
</template>
|
|
<template x-if="!{{ src }}">
|
|
<span x-html="$icon('user', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
</template>
|
|
{% elif src %}
|
|
<img src="{{ src }}" alt="{{ alt }}" class="w-full h-full object-cover" loading="lazy">
|
|
{% else %}
|
|
<span x-html="$icon('user', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
|
|
{#
|
|
User Avatar Card
|
|
================
|
|
An avatar with name and optional subtitle/role.
|
|
|
|
Parameters:
|
|
- src: Image source (Alpine.js expression)
|
|
- name: User name (Alpine.js expression)
|
|
- subtitle: Subtitle/role (Alpine.js expression, optional)
|
|
- size: Avatar size (default: 'md')
|
|
- href: Link URL (optional)
|
|
#}
|
|
{% macro user_avatar_card(src, name, subtitle=none, size='md', href=none) %}
|
|
{% set sizes = {
|
|
'sm': 'w-8 h-8',
|
|
'md': 'w-10 h-10',
|
|
'lg': 'w-12 h-12'
|
|
} %}
|
|
{% set icon_sizes = {
|
|
'sm': 'w-4 h-4',
|
|
'md': 'w-5 h-5',
|
|
'lg': 'w-6 h-6'
|
|
} %}
|
|
{% set text_sizes = {
|
|
'sm': 'text-sm',
|
|
'md': 'text-sm',
|
|
'lg': 'text-base'
|
|
} %}
|
|
{% if href %}
|
|
<a href="{{ href }}" class="flex items-center group">
|
|
{% else %}
|
|
<div class="flex items-center">
|
|
{% endif %}
|
|
<div class="relative inline-flex items-center justify-center {{ sizes[size] }} rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden flex-shrink-0">
|
|
<template x-if="{{ src }}">
|
|
<img :src="{{ src }}" class="w-full h-full object-cover" loading="lazy">
|
|
</template>
|
|
<template x-if="!{{ src }}">
|
|
<span x-html="$icon('user', '{{ icon_sizes[size] }} text-gray-500 dark:text-gray-400')"></span>
|
|
</template>
|
|
</div>
|
|
<div class="ml-3">
|
|
<p class="{{ text_sizes[size] }} font-medium text-gray-900 dark:text-white {% if href %}group-hover:text-purple-600 dark:group-hover:text-purple-400{% endif %}" x-text="{{ name }}"></p>
|
|
{% if subtitle %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400" x-text="{{ subtitle }}"></p>
|
|
{% endif %}
|
|
</div>
|
|
{% if href %}
|
|
</a>
|
|
{% else %}
|
|
</div>
|
|
{% endif %}
|
|
{% endmacro %}
|