Files
orion/app/templates/shared/macros/avatars.html
Samir Boulahtit b0db4d26d8 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>
2025-12-06 19:05:13 +01:00

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 %}