feat: enhance headers and modals shared macros
- headers.html: Add new header layout macros - modals.html: Improve modal component styling and functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -220,3 +220,204 @@
|
||||
</nav>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Refresh Button
|
||||
==============
|
||||
A button with loading state for refresh actions.
|
||||
|
||||
Parameters:
|
||||
- loading_var: Alpine.js variable for loading state (default: 'loading')
|
||||
- onclick: Alpine.js click handler (default: 'refresh()')
|
||||
- label: Button label (default: 'Refresh')
|
||||
- loading_label: Label while loading (default: 'Loading...')
|
||||
- variant: 'primary' | 'secondary' (default: 'primary')
|
||||
#}
|
||||
{% macro refresh_button(loading_var='loading', onclick='refresh()', label='Refresh', loading_label='Loading...', variant='primary') %}
|
||||
{% set variants = {
|
||||
'primary': 'text-white bg-purple-600 border-transparent hover:bg-purple-700 focus:shadow-outline-purple',
|
||||
'secondary': 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:shadow-outline-gray'
|
||||
} %}
|
||||
<button
|
||||
@click="{{ onclick }}"
|
||||
:disabled="{{ loading_var }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 border rounded-lg focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed {{ variants[variant] }}"
|
||||
>
|
||||
<span x-show="!{{ loading_var }}" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="{{ loading_var }}" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="{{ loading_var }} ? '{{ loading_label }}' : '{{ label }}'"></span>
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Action Button (with loading)
|
||||
============================
|
||||
A generic action button with loading state.
|
||||
|
||||
Parameters:
|
||||
- label: Button label
|
||||
- loading_label: Label while loading
|
||||
- loading_var: Alpine.js variable for loading state
|
||||
- onclick: Alpine.js click handler
|
||||
- icon: Button icon (default: 'check')
|
||||
- variant: 'primary' | 'secondary' | 'danger' (default: 'primary')
|
||||
#}
|
||||
{% macro action_button(label, loading_label, loading_var, onclick, icon='check', variant='primary') %}
|
||||
{% set variants = {
|
||||
'primary': 'text-white bg-purple-600 border-transparent hover:bg-purple-700 focus:shadow-outline-purple',
|
||||
'secondary': 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 focus:shadow-outline-gray',
|
||||
'danger': 'text-white bg-red-600 border-transparent hover:bg-red-700 focus:shadow-outline-red'
|
||||
} %}
|
||||
<button
|
||||
@click="{{ onclick }}"
|
||||
:disabled="{{ loading_var }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 border rounded-lg focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed {{ variants[variant] }}"
|
||||
>
|
||||
<span x-show="!{{ loading_var }}" x-html="$icon('{{ icon }}', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="{{ loading_var }}" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="{{ loading_var }} ? '{{ loading_label }}' : '{{ label }}'"></span>
|
||||
</button>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Back Button
|
||||
===========
|
||||
A simple back navigation button.
|
||||
|
||||
Parameters:
|
||||
- url: Back URL
|
||||
- label: Button label (default: 'Back')
|
||||
#}
|
||||
{% macro back_button(url, label='Back') %}
|
||||
<a href="{{ url }}"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray">
|
||||
<span x-html="$icon('arrow-left', 'w-4 h-4 mr-2')"></span>
|
||||
{{ label }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Page Header (Flexible)
|
||||
======================
|
||||
A flexible page header that accepts custom content via caller().
|
||||
Use this when you need custom action buttons or dynamic content.
|
||||
|
||||
Parameters:
|
||||
- title: Page title (static string)
|
||||
- title_var: Alpine.js variable for dynamic title (use instead of title)
|
||||
- subtitle: Page subtitle (static string)
|
||||
- subtitle_var: Alpine.js variable for dynamic subtitle
|
||||
- subtitle_show: Alpine.js condition for showing subtitle (e.g., 'user')
|
||||
|
||||
Usage:
|
||||
{% call page_header_flex(title='Dashboard') %}
|
||||
{{ refresh_button() }}
|
||||
{% endcall %}
|
||||
|
||||
{% call page_header_flex(title_var="user?.name || 'User'", subtitle_show='user') %}
|
||||
{{ back_button('/admin/users') }}
|
||||
{% endcall %}
|
||||
#}
|
||||
{% macro page_header_flex(title=none, title_var=none, subtitle=none, subtitle_var=none, subtitle_show=none) %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
{% if title_var %}
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="{{ title_var }}"></h2>
|
||||
{% else %}
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h2>
|
||||
{% endif %}
|
||||
{% if subtitle_var %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1"{% if subtitle_show %} x-show="{{ subtitle_show }}"{% endif %} x-text="{{ subtitle_var }}"></p>
|
||||
{% elif subtitle %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1"{% if subtitle_show %} x-show="{{ subtitle_show }}"{% endif %}>{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Page Header (with Refresh)
|
||||
==========================
|
||||
A common pattern: static header with refresh button.
|
||||
|
||||
Parameters:
|
||||
- title: Page title
|
||||
- subtitle: Page subtitle (optional)
|
||||
- loading_var: Alpine.js variable for loading state (default: 'loading')
|
||||
- refresh_action: Alpine.js refresh action (default: 'refresh()')
|
||||
#}
|
||||
{% macro page_header_refresh(title, subtitle=none, loading_var='loading', refresh_action='refresh()') %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h2>
|
||||
{% if subtitle %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
{{ refresh_button(loading_var=loading_var, onclick=refresh_action) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Detail Page Header
|
||||
==================
|
||||
Common pattern for detail pages with dynamic title and back button.
|
||||
|
||||
Parameters:
|
||||
- title_var: Alpine.js expression for title (e.g., "user?.name || 'User Details'")
|
||||
- subtitle_template: Jinja template string for subtitle (rendered as-is)
|
||||
- subtitle_show: Alpine.js condition for showing subtitle
|
||||
- back_url: URL for back button
|
||||
- back_label: Back button label (default: 'Back')
|
||||
#}
|
||||
{% macro detail_page_header(title_var, back_url, subtitle_show=none, back_label='Back') %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="{{ title_var }}"></h2>
|
||||
{% if caller is defined %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1"{% if subtitle_show %} x-show="{{ subtitle_show }}"{% endif %}>
|
||||
{{ caller() }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ back_button(back_url, back_label) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Edit Page Header
|
||||
================
|
||||
Common pattern for edit pages with static title, dynamic subtitle, and back button.
|
||||
|
||||
Parameters:
|
||||
- title: Static title (e.g., 'Edit Vendor')
|
||||
- subtitle_var: Alpine.js expression for subtitle parts
|
||||
- subtitle_show: Alpine.js condition for showing subtitle
|
||||
- back_url: URL for back button
|
||||
- back_label: Back button label (default: 'Back')
|
||||
#}
|
||||
{% macro edit_page_header(title, back_url, subtitle_show=none, back_label='Back') %}
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">{{ title }}</h2>
|
||||
{% if caller is defined %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1"{% if subtitle_show %} x-show="{{ subtitle_show }}"{% endif %}>
|
||||
{{ caller() }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ back_button(back_url, back_label) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
Usage:
|
||||
{% from 'shared/macros/modals.html' import modal, confirm_modal, form_modal %}
|
||||
|
||||
{# Basic modal #}
|
||||
Basic modal:
|
||||
{% call modal('editModal', 'Edit User', 'isEditModalOpen') %}
|
||||
<p>Modal content here</p>
|
||||
{% endcall %}
|
||||
|
||||
{# Confirmation modal #}
|
||||
Confirmation modal:
|
||||
{{ confirm_modal('deleteModal', 'Delete User', 'Are you sure?', 'deleteUser()', 'isDeleteModalOpen') }}
|
||||
|
||||
Required Alpine.js:
|
||||
@@ -475,3 +475,139 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{#
|
||||
Job Details Modal
|
||||
=================
|
||||
A mobile-friendly modal for displaying import job details.
|
||||
Used in marketplace and imports admin pages.
|
||||
|
||||
Parameters:
|
||||
- show_var: Alpine.js variable controlling visibility (default: 'showJobModal')
|
||||
- job_var: Alpine.js variable containing the job data (default: 'selectedJob')
|
||||
- close_action: Alpine.js action to close modal (default: 'closeJobModal()')
|
||||
- get_vendor_name: Function to get vendor name from ID (default: 'getVendorName')
|
||||
- show_created_by: Whether to show Created By field (default: false)
|
||||
|
||||
Required Alpine.js state:
|
||||
- showJobModal: boolean
|
||||
- selectedJob: object with job data
|
||||
- closeJobModal(): function to close and clear
|
||||
- getVendorName(id): function to resolve vendor name
|
||||
- formatDate(date): function to format dates
|
||||
#}
|
||||
{% macro job_details_modal(show_var='showJobModal', job_var='selectedJob', close_action='closeJobModal()', get_vendor_name='getVendorName', show_created_by=false) %}
|
||||
<div x-show="{{ show_var }}"
|
||||
x-cloak
|
||||
@click.away="{{ close_action }}"
|
||||
class="fixed inset-0 z-30 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div @click.away="{{ close_action }}"
|
||||
class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-2xl"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-1/2"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0 transform translate-y-1/2">
|
||||
{# Modal Header #}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Import Job Details
|
||||
</h3>
|
||||
<button @click="{{ close_action }}" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<span x-html="$icon('close', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Modal Content #}
|
||||
<div x-show="{{ job_var }}" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Job ID</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.id"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Vendor</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ get_vendor_name }}({{ job_var }}?.vendor_id)"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Marketplace</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.marketplace"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Status</p>
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100': {{ job_var }}?.status === 'completed',
|
||||
'text-blue-700 bg-blue-100': {{ job_var }}?.status === 'processing',
|
||||
'text-yellow-700 bg-yellow-100': {{ job_var }}?.status === 'pending',
|
||||
'text-red-700 bg-red-100': {{ job_var }}?.status === 'failed',
|
||||
'text-orange-700 bg-orange-100': {{ job_var }}?.status === 'completed_with_errors'
|
||||
}"
|
||||
x-text="{{ job_var }}?.status.replace('_', ' ').toUpperCase()">
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Source URL</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100 break-all" x-text="{{ job_var }}?.source_url"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Imported</p>
|
||||
<p class="text-sm text-green-600 dark:text-green-400" x-text="{{ job_var }}?.imported_count"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Updated</p>
|
||||
<p class="text-sm text-blue-600 dark:text-blue-400" x-text="{{ job_var }}?.updated_count"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Errors</p>
|
||||
<p class="text-sm text-red-600 dark:text-red-400" x-text="{{ job_var }}?.error_count"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Processed</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.total_processed"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Started At</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.started_at ? formatDate({{ job_var }}.started_at) : 'Not started'"></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Completed At</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.completed_at ? formatDate({{ job_var }}.completed_at) : 'Not completed'"></p>
|
||||
</div>
|
||||
{% if show_created_by %}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Created By</p>
|
||||
<p class="text-sm text-gray-900 dark:text-gray-100" x-text="{{ job_var }}?.created_by_name || 'System'"></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Error Details #}
|
||||
<div x-show="{{ job_var }}?.error_details?.length > 0" class="mt-4">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">Error Details</p>
|
||||
<div class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg max-h-48 overflow-y-auto">
|
||||
<pre class="text-xs text-red-700 dark:text-red-300 whitespace-pre-wrap" x-text="JSON.stringify({{ job_var }}?.error_details, null, 2)"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Modal Footer #}
|
||||
<div class="flex justify-end mt-6">
|
||||
<button
|
||||
@click="{{ close_action }}"
|
||||
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 border border-gray-300 rounded-lg dark:text-gray-400 dark:border-gray-700 hover:border-gray-500 focus:outline-none"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
Reference in New Issue
Block a user