Files
orion/app/templates/shared/macros/inputs.html
Samir Boulahtit f3dc143f1d chore: add shared components and update docs
- Add vendor selector component for admin pages
- Add input macros for form handling
- Add truck icon for shipping UI
- Update vendor operations expansion plan
- Update mkdocs configuration
- Update dependencies

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 21:05:50 +01:00

285 lines
11 KiB
HTML

{#
Input Macros
============
Reusable input components with Alpine.js integration.
Usage:
{% from 'shared/macros/inputs.html' import search_autocomplete %}
{{ search_autocomplete(
search_var='userSearchQuery',
results_var='userSearchResults',
show_dropdown_var='showUserDropdown',
loading_var='searchingUsers',
disabled_var='transferring',
select_action='selectUser(user)',
display_field='username',
secondary_field='email',
placeholder='Search by name or email...'
) }}
#}
{#
Search Autocomplete
===================
A search input with dropdown results for autocomplete functionality.
Parameters:
- search_var: Alpine.js variable for search query (required)
- results_var: Alpine.js variable containing search results array (required)
- show_dropdown_var: Alpine.js variable controlling dropdown visibility (required)
- loading_var: Alpine.js variable for loading state (default: 'searching')
- disabled_var: Alpine.js variable for disabled state (default: none)
- search_action: Alpine.js action on input (default: 'search()')
- select_action: Alpine.js action when item selected, receives 'item' (required)
- selected_check: Alpine.js expression to check if item is selected (optional)
- display_field: Field name for primary display text (default: 'name')
- secondary_field: Field name for secondary text (optional)
- id_field: Field name for item ID (default: 'id')
- placeholder: Input placeholder text (default: 'Search...')
- min_chars: Minimum characters before showing results (default: 2)
- no_results_text: Text when no results found (default: 'No results found')
- loading_text: Text while loading (default: 'Searching...')
#}
{% macro search_autocomplete(
search_var,
results_var,
show_dropdown_var,
loading_var='searching',
disabled_var=none,
search_action='search()',
select_action='selectItem(item)',
selected_check=none,
display_field='name',
secondary_field=none,
id_field='id',
placeholder='Search...',
min_chars=2,
no_results_text='No results found',
loading_text='Searching...'
) %}
<div class="relative">
<input
type="text"
x-model="{{ search_var }}"
@input="{{ search_action }}"
@focus="{{ show_dropdown_var }} = true"
{% if disabled_var %}:disabled="{{ disabled_var }}"{% endif %}
placeholder="{{ placeholder }}"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
>
{# Search Results Dropdown #}
<div
x-show="{{ show_dropdown_var }} && {{ results_var }}.length > 0"
x-cloak
@click.away="{{ show_dropdown_var }} = false"
class="absolute z-10 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-y-auto"
>
<template x-for="item in {{ results_var }}" :key="item.{{ id_field }}">
<button
type="button"
@click="{{ select_action }}"
class="w-full px-4 py-2 text-left text-sm hover:bg-purple-50 dark:hover:bg-gray-600 focus:outline-none"
{% if selected_check %}:class="{ 'bg-purple-50 dark:bg-gray-600': {{ selected_check }} }"{% endif %}
>
<div class="font-medium text-gray-700 dark:text-gray-200" x-text="item.{{ display_field }}"></div>
{% if secondary_field %}
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="item.{{ secondary_field }}"></div>
{% endif %}
</button>
</template>
</div>
{# No Results #}
<div
x-show="{{ show_dropdown_var }} && {{ search_var }}.length >= {{ min_chars }} && {{ results_var }}.length === 0 && !{{ loading_var }}"
class="absolute z-10 w-full mt-1 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg text-sm text-gray-500 dark:text-gray-400"
>
{{ no_results_text }}
</div>
{# Loading #}
<div
x-show="{{ loading_var }}"
class="absolute z-10 w-full mt-1 px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg text-sm text-gray-500 dark:text-gray-400"
>
<span x-html="$icon('spinner', 'w-4 h-4 inline mr-2')"></span>
{{ loading_text }}
</div>
</div>
{% endmacro %}
{#
Selected Item Display
=====================
A display component for showing the currently selected item with clear button.
Parameters:
- selected_var: Alpine.js variable containing selected item (required)
- display_field: Field name for primary display text (default: 'name')
- secondary_field: Field name for secondary text (optional)
- clear_action: Alpine.js action to clear selection (required)
- label: Label text before the selection (default: 'Selected:')
#}
{% macro selected_item_display(
selected_var,
display_field='name',
secondary_field=none,
clear_action='clearSelection()',
label='Selected:'
) %}
<div x-show="{{ selected_var }}" class="mt-2 p-2 bg-purple-50 dark:bg-purple-900 rounded-lg text-sm">
<span class="text-purple-700 dark:text-purple-300">
{{ label }} <strong x-text="{{ selected_var }}?.{{ display_field }}"></strong>
{% if secondary_field %}
(<span x-text="{{ selected_var }}?.{{ secondary_field }}"></span>)
{% endif %}
</span>
<button type="button" @click="{{ clear_action }}" class="ml-2 text-purple-600 hover:text-purple-800 dark:text-purple-400">
<span x-html="$icon('x', 'w-4 h-4 inline')"></span>
</button>
</div>
{% endmacro %}
{#
Number Stepper
==============
A number input with +/- buttons for incrementing/decrementing values.
Useful for quantity selectors in carts, product pages, batch sizes, etc.
Parameters:
- model: Alpine.js x-model variable (required)
- min: Minimum allowed value (default: 1)
- max: Maximum allowed value (default: none - unlimited)
- step: Increment/decrement step (default: 1)
- size: 'sm' | 'md' | 'lg' (default: 'md')
- disabled_var: Alpine.js variable for disabled state (optional)
- name: Input name for form submission (optional)
- id: Input id attribute (optional)
- label: Accessible label for screen readers (default: 'Quantity')
Usage:
{{ number_stepper(model='quantity', min=1, max=99) }}
{{ number_stepper(model='cart.items[index].qty', min=1, max='item.stock', size='sm') }}
{{ number_stepper(model='batchSize', min=100, max=5000, step=100, size='lg') }}
#}
{% macro number_stepper(
model,
min=1,
max=none,
step=1,
size='md',
disabled_var=none,
name=none,
id=none,
label='Quantity'
) %}
{% set sizes = {
'sm': {
'btn': 'px-2 py-1',
'input': 'px-2 py-1 text-xs w-12',
'icon': 'w-3 h-3'
},
'md': {
'btn': 'px-3 py-2',
'input': 'px-3 py-2 text-sm w-16',
'icon': 'w-4 h-4'
},
'lg': {
'btn': 'px-4 py-3',
'input': 'px-4 py-3 text-base w-20',
'icon': 'w-5 h-5'
}
} %}
{% set s = sizes[size] %}
<div class="flex" role="group" aria-label="{{ label }}">
<button
type="button"
@click="{{ model }} = Math.max({{ min }}, {{ model }} - {{ step }})"
{% if disabled_var %}:disabled="{{ disabled_var }}"{% endif %}
{% if max %}:disabled="{{ model }} <= {{ min }}"{% endif %}
class="{{ s.btn }} text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-600 border-r-0 rounded-l-md hover:bg-gray-200 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:z-10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
aria-label="Decrease {{ label|lower }}"
>
<span x-html="$icon('minus', '{{ s.icon }}')"></span>
</button>
<input
type="number"
x-model.number="{{ model }}"
{% if name %}name="{{ name }}"{% endif %}
{% if id %}id="{{ id }}"{% endif %}
min="{{ min }}"
{% if max %}:max="{{ max }}" max="{{ max }}"{% endif %}
step="{{ step }}"
{% if disabled_var %}:disabled="{{ disabled_var }}"{% endif %}
class="no-spinner {{ s.input }} text-center text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border-y border-gray-300 dark:border-gray-600 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:z-10"
aria-label="{{ label }}"
/>
<button
type="button"
@click="{{ model }} = {% if max %}Math.min({{ max }}, {{ model }} + {{ step }}){% else %}{{ model }} + {{ step }}{% endif %}"
{% if disabled_var %}:disabled="{{ disabled_var }}"{% endif %}
{% if max %}:disabled="{{ model }} >= {{ max }}"{% endif %}
class="{{ s.btn }} text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-600 border-l-0 rounded-r-md hover:bg-gray-200 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:z-10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
aria-label="Increase {{ label|lower }}"
>
<span x-html="$icon('plus', '{{ s.icon }}')"></span>
</button>
</div>
{% endmacro %}
{#
Vendor Selector (Tom Select)
============================
An async searchable vendor selector using Tom Select.
Searches vendors by name and code with autocomplete.
Prerequisites:
- Tom Select CSS/JS must be loaded (included in admin/base.html)
- vendor-selector.js must be loaded
Parameters:
- ref_name: Alpine.js x-ref name for the select element (default: 'vendorSelect')
- id: HTML id attribute (default: 'vendor-select')
- placeholder: Placeholder text (default: 'Search vendor by name or code...')
- width: CSS width class (default: 'w-80')
- on_init: JS callback name when Tom Select is initialized (optional)
Usage:
{% from 'shared/macros/inputs.html' import vendor_selector %}
{{ vendor_selector(
ref_name='vendorSelect',
placeholder='Select a vendor...',
width='w-96'
) }}
// In your Alpine.js component init():
this.$nextTick(() => {
initVendorSelector(this.$refs.vendorSelect, {
onSelect: (vendor) => this.onVendorSelected(vendor),
onClear: () => this.onVendorCleared()
});
});
#}
{% macro vendor_selector(
ref_name='vendorSelect',
id='vendor-select',
placeholder='Search vendor by name or code...',
width='w-80'
) %}
<div class="{{ width }}">
<select
id="{{ id }}"
x-ref="{{ ref_name }}"
placeholder="{{ placeholder }}"
aria-label="Vendor selector"
></select>
</div>
{% endmacro %}