diff --git a/app/templates/shared/macros/inputs.html b/app/templates/shared/macros/inputs.html
new file mode 100644
index 00000000..22fc0aad
--- /dev/null
+++ b/app/templates/shared/macros/inputs.html
@@ -0,0 +1,233 @@
+{#
+ 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...'
+) %}
+
+
+
+ {# Search Results Dropdown #}
+
+
+
+
+
+
+ {# No Results #}
+
+ {{ no_results_text }}
+
+
+ {# Loading #}
+
+
+ {{ loading_text }}
+
+
+{% 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:'
+) %}
+
+
+ {{ label }}
+ {% if secondary_field %}
+ ()
+ {% endif %}
+
+
+
+{% 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] %}
+
+
+
+
+
+{% endmacro %}
diff --git a/app/templates/shared/macros/tabs.html b/app/templates/shared/macros/tabs.html
new file mode 100644
index 00000000..0bcfbbed
--- /dev/null
+++ b/app/templates/shared/macros/tabs.html
@@ -0,0 +1,78 @@
+{# app/templates/shared/macros/tabs.html #}
+{# Tab navigation components for consistent UI across admin pages #}
+
+{#
+ Tab navigation wrapper (standalone)
+ Usage:
+ {% call tabs_nav() %}
+ {{ tab_button('tab1', 'Tab 1', icon='home') }}
+ {{ tab_button('tab2', 'Tab 2', icon='cog') }}
+ {% endcall %}
+#}
+{% macro tabs_nav(tab_var='activeTab', class='') %}
+
+
+
+
+
+{% endmacro %}
+
+{#
+ Tab navigation wrapper (inline, for use inside cards/flex containers)
+ Usage:
+
+ {% call tabs_inline() %}
+ {{ tab_button('all', 'All', count_var='items.length') }}
+ {% endcall %}
+
+
+#}
+{% macro tabs_inline(tab_var='activeTab') %}
+
+ {{ caller() }}
+
+{% endmacro %}
+
+{#
+ Individual tab button
+ Args:
+ id: Tab identifier (used for activeTab comparison)
+ label: Display text
+ tab_var: Alpine.js variable name for active tab (default: 'activeTab')
+ icon: Optional icon name (uses $icon helper)
+ count_var: Optional Alpine.js variable for count badge
+ onclick: Optional custom click handler (overrides default tab switching)
+#}
+{% macro tab_button(id, label, tab_var='activeTab', icon=none, count_var=none, onclick=none) %}
+
+{% endmacro %}
+
+{#
+ Tab panel wrapper (for content that shows/hides based on active tab)
+ Usage:
+ {{ tab_panel('tab1') }}
+ Tab 1 content
+ {{ endtab_panel() }}
+
+ Or simply use x-show directly:
+
+ Content
+
+#}
+{% macro tab_panel(id, tab_var='activeTab', transition=true) %}
+
+{% endmacro %}
+
+{% macro endtab_panel() %}
+
+{% endmacro %}