Add missing features identified in TailAdmin gap analysis: cards.html: - stat_card_with_trend: Stats card with up/down trend indicator and percentage - card_with_menu: Card with dropdown menu in header - card_with_menu_simple: Simplified version with menu_items parameter tables.html: - sortable_table_header: Table header with sortable columns, sort indicators, and Alpine.js integration for sort state management forms.html: - searchable_select: Dropdown with search/filter functionality - multi_select: Multi-select dropdown with tag display This completes feature parity with TailAdmin free template. The tailadmin-free-tailwind-dashboard-template folder can now be deleted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
286 lines
9.1 KiB
HTML
286 lines
9.1 KiB
HTML
{#
|
|
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 %}
|
|
|
|
|
|
{#
|
|
Sortable Table Header
|
|
=====================
|
|
Renders a table header with sortable columns.
|
|
|
|
Parameters:
|
|
- columns: List of column definitions [{'label': '', 'key': '', 'sortable': true/false, 'width': ''}]
|
|
- sort_key_var: Alpine.js variable for current sort key (default: 'sortKey')
|
|
- sort_dir_var: Alpine.js variable for sort direction (default: 'sortDir')
|
|
- on_sort: Alpine.js handler when column is clicked (default: 'sortBy')
|
|
|
|
Usage:
|
|
{{ sortable_table_header([
|
|
{'label': 'Name', 'key': 'name', 'sortable': true},
|
|
{'label': 'Email', 'key': 'email', 'sortable': true},
|
|
{'label': 'Status', 'key': 'status', 'sortable': false},
|
|
{'label': 'Actions', 'key': 'actions', 'sortable': false, 'width': 'w-24'}
|
|
]) }}
|
|
|
|
Required Alpine.js:
|
|
sortKey: 'name',
|
|
sortDir: 'asc',
|
|
sortBy(key) {
|
|
if (this.sortKey === key) {
|
|
this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sortKey = key;
|
|
this.sortDir = 'asc';
|
|
}
|
|
this.loadItems();
|
|
}
|
|
#}
|
|
{% macro sortable_table_header(columns, sort_key_var='sortKey', sort_dir_var='sortDir', on_sort='sortBy') %}
|
|
<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 col in columns %}
|
|
<th class="px-4 py-3 {{ col.width if col.width else '' }}">
|
|
{% if col.sortable %}
|
|
<button
|
|
@click="{{ on_sort }}('{{ col.key }}')"
|
|
class="flex items-center space-x-1 hover:text-gray-700 dark:hover:text-gray-200 transition-colors group"
|
|
>
|
|
<span>{{ col.label }}</span>
|
|
<span class="flex flex-col">
|
|
<svg
|
|
class="w-3 h-3 -mb-1 transition-colors"
|
|
:class="{{ sort_key_var }} === '{{ col.key }}' && {{ sort_dir_var }} === 'asc' ? 'text-purple-600 dark:text-purple-400' : 'text-gray-300 dark:text-gray-600 group-hover:text-gray-400'"
|
|
fill="currentColor" viewBox="0 0 20 20"
|
|
>
|
|
<path d="M5 12l5-5 5 5H5z"/>
|
|
</svg>
|
|
<svg
|
|
class="w-3 h-3 -mt-1 transition-colors"
|
|
:class="{{ sort_key_var }} === '{{ col.key }}' && {{ sort_dir_var }} === 'desc' ? 'text-purple-600 dark:text-purple-400' : 'text-gray-300 dark:text-gray-600 group-hover:text-gray-400'"
|
|
fill="currentColor" viewBox="0 0 20 20"
|
|
>
|
|
<path d="M15 8l-5 5-5-5h10z"/>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
{% else %}
|
|
{{ col.label }}
|
|
{% endif %}
|
|
</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 %}
|