feat: add action_dropdown macro with loading state support

- Create action_dropdown macro in dropdowns.html supporting:
  - Loading/disabled state via loading_var parameter
  - Custom loading label
  - Icon support
  - Primary/secondary variants
- Update code quality dashboard to use new macro
- Add Dropdowns section to components page with examples:
  - Basic dropdown
  - Action dropdown with loading state
  - Context menu (3-dot)
  - Variant showcase (primary, secondary, ghost)

Architecture validation now passes with 0 errors and 0 warnings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 21:13:08 +01:00
parent d50b154823
commit 245040d256
4 changed files with 225 additions and 38 deletions

View File

@@ -348,3 +348,81 @@
<span x-show="{{ selected_var }} === '{{ value }}'" x-html="$icon('check', 'w-4 h-4')"></span>
</button>
{% endmacro %}
{#
Action Dropdown
===============
A dropdown with loading/disabled state support for action buttons.
Uses external Alpine.js state from parent component.
Parameters:
- label: Button label
- loading_label: Label shown when loading (default: 'Loading...')
- open_var: Alpine.js variable for open state
- loading_var: Alpine.js variable for loading/disabled state
- icon: Button icon (default: 'chevron-down')
- position: 'left' | 'right' (default: 'right')
- variant: 'primary' | 'secondary' (default: 'primary')
- width: Dropdown width class (default: 'w-48')
Usage:
{% call action_dropdown(
label='Run Scan',
loading_label='Scanning...',
open_var='scanDropdownOpen',
loading_var='scanning',
icon='search'
) %}
{{ dropdown_item('Run All', 'runScan("all")') }}
{{ dropdown_item('Architecture Only', 'runScan("architecture")') }}
{% endcall %}
#}
{% macro action_dropdown(label, loading_label='Loading...', open_var='isDropdownOpen', loading_var='isLoading', icon=none, position='right', variant='primary', width='w-48') %}
{% set variants = {
'primary': 'text-white bg-purple-600 hover:bg-purple-700 border-transparent focus:shadow-outline-purple',
'secondary': 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 border-gray-300 dark:border-gray-600 focus:shadow-outline-gray'
} %}
{% set positions = {
'left': 'left-0',
'right': 'right-0'
} %}
<div class="relative">
<button
@click="{{ open_var }} = !{{ open_var }}"
@click.outside="{{ open_var }} = false"
:disabled="{{ loading_var }}"
type="button"
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] }}"
>
<template x-if="!{{ loading_var }}">
<span class="flex items-center">
{% if icon %}
<span x-html="$icon('{{ icon }}', 'w-4 h-4 mr-2')"></span>
{% endif %}
{{ label }}
<span x-html="$icon('chevron-down', 'w-4 h-4 ml-1')"></span>
</span>
</template>
<template x-if="{{ loading_var }}">
<span class="flex items-center">
<span x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
{{ loading_label }}
</span>
</template>
</button>
<div
x-show="{{ open_var }}"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute z-50 mt-2 {{ width }} {{ positions[position] }} bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1"
>
{{ caller() }}
</div>
</div>
{% endmacro %}