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

@@ -2,6 +2,7 @@
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import loading_state, error_state, alert_dynamic %}
{% from 'shared/macros/headers.html' import page_header_flex, refresh_button %}
{% from 'shared/macros/dropdowns.html' import action_dropdown, dropdown_item %}
{% block title %}Code Quality Dashboard{% endblock %}
@@ -14,44 +15,18 @@
{% block content %}
{% call page_header_flex(title='Code Quality Dashboard', subtitle='Unified code quality tracking: architecture, security, and performance') %}
{{ refresh_button(variant='secondary') }}
{# Custom dropdown: disabled state + template switching not supported by macro #}
<div x-data="{ scanDropdownOpen: false }" class="relative">
<button @click="scanDropdownOpen = !scanDropdownOpen"
:disabled="scanning"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50">
<template x-if="!scanning">
<span class="flex items-center">
<span x-html="$icon('search', 'w-4 h-4 mr-2')"></span>
Run Scan
<span x-html="$icon('chevron-down', 'w-4 h-4 ml-1')"></span>
</span>
</template>
<template x-if="scanning">
<span>Scanning...</span>
</template>
</button>
<div x-show="scanDropdownOpen"
@click.away="scanDropdownOpen = false"
x-transition
class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-10 border border-gray-200 dark:border-gray-700">
<button @click="runScan('all'); scanDropdownOpen = false"
class="block w-full px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg">
Run All Validators
</button>
<button @click="runScan('architecture'); scanDropdownOpen = false"
class="block w-full px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
Architecture Only
</button>
<button @click="runScan('security'); scanDropdownOpen = false"
class="block w-full px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
Security Only
</button>
<button @click="runScan('performance'); scanDropdownOpen = false"
class="block w-full px-4 py-2 text-sm text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg">
Performance Only
</button>
</div>
</div>
{% call action_dropdown(
label='Run Scan',
loading_label='Scanning...',
open_var='scanDropdownOpen',
loading_var='scanning',
icon='search'
) %}
{{ dropdown_item('Run All Validators', 'runScan("all"); scanDropdownOpen = false') }}
{{ dropdown_item('Architecture Only', 'runScan("architecture"); scanDropdownOpen = false') }}
{{ dropdown_item('Security Only', 'runScan("security"); scanDropdownOpen = false') }}
{{ dropdown_item('Performance Only', 'runScan("performance"); scanDropdownOpen = false') }}
{% endcall %}
{% endcall %}
{{ loading_state('Loading dashboard...') }}

View File

@@ -2136,6 +2136,139 @@ goToPage(n) { if (n !== '...' && n >= 1 && n <= this.totalPages) { this.paginati
</div>
</section>
<!-- DROPDOWNS SECTION -->
<section id="dropdowns" class="scroll-mt-24">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-200 mb-6 flex items-center">
<span x-html="$icon('chevron-down', 'w-6 h-6 mr-2 text-purple-600 dark:text-purple-400')"></span>
Dropdowns
</h2>
<div class="mb-6 p-4 bg-blue-50 dark:bg-gray-700 border border-blue-200 dark:border-gray-600 rounded-lg">
<div class="flex items-start">
<span x-html="$icon('information-circle', 'w-5 h-5 text-blue-600 dark:text-blue-400 mr-2 flex-shrink-0 mt-0.5')"></span>
<div>
<p class="text-sm text-blue-800 dark:text-gray-200">
<strong>Dropdown Macros:</strong> Use macros from <code class="bg-blue-100 dark:bg-gray-600 px-1 rounded">shared/macros/dropdowns.html</code> for consistent dropdown styling.
</p>
</div>
</div>
</div>
<!-- Basic Dropdown -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Basic Dropdown</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Standard dropdown with items. Uses its own Alpine.js state.</p>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3">
{% from 'shared/macros/dropdowns.html' import dropdown, dropdown_item %}
{% call dropdown('Actions', 'demoDropdown1') %}
{{ dropdown_item('Edit', "alert('Edit clicked')", icon='pencil') }}
{{ dropdown_item('Duplicate', "alert('Duplicate clicked')", icon='duplicate') }}
{{ dropdown_item('Delete', "alert('Delete clicked')", icon='trash', variant='danger') }}
{% endcall %}
</div>
<button @click="copyCode(`{% raw %}{% from 'shared/macros/dropdowns.html' import dropdown, dropdown_item %}
{% call dropdown('Actions', 'isDropdownOpen') %}
{{ dropdown_item('Edit', 'edit()', icon='pencil') }}
{{ dropdown_item('Duplicate', 'duplicate()', icon='duplicate') }}
{{ dropdown_item('Delete', 'delete()', icon='trash', variant='danger') }}
{% endcall %}{% endraw %}`)" class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 flex items-center">
<span x-html="$icon('duplicate', 'w-4 h-4 mr-1')"></span>
Copy Code
</button>
</div>
<!-- Action Dropdown with Loading State -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Action Dropdown with Loading State</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Dropdown that supports disabled/loading state. Uses external Alpine.js state from parent component.</p>
<div x-data="{ demoLoading: false, demoDropdownOpen: false }" class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3">
<div class="flex items-center gap-4">
{% from 'shared/macros/dropdowns.html' import action_dropdown, dropdown_item %}
{% call action_dropdown(
label='Run Action',
loading_label='Processing...',
open_var='demoDropdownOpen',
loading_var='demoLoading',
icon='play'
) %}
{{ dropdown_item('Option 1', "demoDropdownOpen = false; demoLoading = true; setTimeout(() => demoLoading = false, 2000)") }}
{{ dropdown_item('Option 2', "demoDropdownOpen = false; alert('Option 2')") }}
{{ dropdown_item('Option 3', "demoDropdownOpen = false; alert('Option 3')") }}
{% endcall %}
<span class="text-xs text-gray-500">(Click Option 1 to see loading state)</span>
</div>
</div>
<button @click="copyCode(`{% raw %}{% from 'shared/macros/dropdowns.html' import action_dropdown, dropdown_item %}
{% call action_dropdown(
label='Run Scan',
loading_label='Scanning...',
open_var='scanDropdownOpen',
loading_var='scanning',
icon='search'
) %}
{{ dropdown_item('Run All', 'runScan(\"all\"); scanDropdownOpen = false') }}
{{ dropdown_item('Run Selected', 'runScan(\"selected\"); scanDropdownOpen = false') }}
{% endcall %}{% endraw %}`)" class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 flex items-center">
<span x-html="$icon('duplicate', 'w-4 h-4 mr-1')"></span>
Copy Code
</button>
</div>
<!-- Context Menu -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Context Menu (3-dot)</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Icon-only dropdown commonly used for row actions in tables.</p>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3">
{% from 'shared/macros/dropdowns.html' import context_menu, dropdown_item, dropdown_divider %}
{% call context_menu('demoContext', 'demoContextOpen') %}
{{ dropdown_item('View', "alert('View')", icon='eye') }}
{{ dropdown_item('Edit', "alert('Edit')", icon='pencil') }}
{{ dropdown_divider() }}
{{ dropdown_item('Delete', "alert('Delete')", icon='trash', variant='danger') }}
{% endcall %}
</div>
<button @click="copyCode(`{% raw %}{% from 'shared/macros/dropdowns.html' import context_menu, dropdown_item, dropdown_divider %}
{% call context_menu('rowMenu', 'isMenuOpen') %}
{{ dropdown_item('View', 'view()', icon='eye') }}
{{ dropdown_item('Edit', 'edit()', icon='pencil') }}
{{ dropdown_divider() }}
{{ dropdown_item('Delete', 'delete()', icon='trash', variant='danger') }}
{% endcall %}{% endraw %}`)" class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 flex items-center">
<span x-html="$icon('duplicate', 'w-4 h-4 mr-1')"></span>
Copy Code
</button>
</div>
<!-- Dropdown Variants -->
<div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Dropdown Variants</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Primary, secondary, and ghost variants.</p>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3 flex gap-4">
{% from 'shared/macros/dropdowns.html' import dropdown, dropdown_item %}
{% call dropdown('Primary', 'demoPrimary', variant='primary') %}
{{ dropdown_item('Option 1', "alert('1')") }}
{{ dropdown_item('Option 2', "alert('2')") }}
{% endcall %}
{% call dropdown('Secondary', 'demoSecondary', variant='secondary') %}
{{ dropdown_item('Option 1', "alert('1')") }}
{{ dropdown_item('Option 2', "alert('2')") }}
{% endcall %}
{% call dropdown('Ghost', 'demoGhost', variant='ghost') %}
{{ dropdown_item('Option 1', "alert('1')") }}
{{ dropdown_item('Option 2', "alert('2')") }}
{% endcall %}
</div>
<button @click="copyCode(`{% raw %}{% call dropdown('Label', 'isOpen', variant='primary') %}...{% endcall %}
{% call dropdown('Label', 'isOpen', variant='secondary') %}...{% endcall %}
{% call dropdown('Label', 'isOpen', variant='ghost') %}...{% endcall %}{% endraw %}`)" class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 flex items-center">
<span x-html="$icon('duplicate', 'w-4 h-4 mr-1')"></span>
Copy Code
</button>
</div>
</div>
</section>
<!-- CARDS SECTION -->
<section id="cards" class="scroll-mt-24">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">

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

View File

@@ -27,6 +27,7 @@ function adminComponents() {
{ id: 'tabs', name: 'Tabs', icon: 'view-boards' },
{ id: 'forms', name: 'Forms', icon: 'clipboard-list' },
{ id: 'buttons', name: 'Buttons', icon: 'cursor-click' },
{ id: 'dropdowns', name: 'Dropdowns', icon: 'chevron-down' },
{ id: 'cards', name: 'Cards', icon: 'collection' },
{ id: 'badges', name: 'Badges', icon: 'tag' },
{ id: 'tables', name: 'Tables', icon: 'table' },