diff --git a/docs/frontend/cdn-fallback-strategy.md b/docs/frontend/cdn-fallback-strategy.md index 591c2969..805265b7 100644 --- a/docs/frontend/cdn-fallback-strategy.md +++ b/docs/frontend/cdn-fallback-strategy.md @@ -13,11 +13,21 @@ All three frontends (Shop, Vendor, Admin) implement a robust CDN fallback strate The following assets are loaded from CDN with automatic fallback to local copies: +### Core Assets (Always Loaded) + | Asset | CDN Source | Local Fallback | |-------|-----------|----------------| | **Tailwind CSS** | `https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css` | `static/shared/css/tailwind.min.css` | | **Alpine.js** | `https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js` | `static/shared/js/vendor/alpine.min.js` | +### Optional Assets (Loaded On Demand) + +| Asset | CDN Source | Local Fallback | Used For | +|-------|-----------|----------------|----------| +| **Chart.js** | `https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js` | `static/shared/js/vendor/chart.umd.min.js` | Charts macros | +| **Flatpickr JS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js` | `static/shared/js/vendor/flatpickr.min.js` | Datepicker macros | +| **Flatpickr CSS** | `https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css` | `static/shared/css/vendor/flatpickr.min.css` | Datepicker styling | + ## Implementation ### Tailwind CSS Fallback @@ -71,25 +81,108 @@ Alpine.js uses dynamic script loading with error handling: 6. Creates new script element pointing to local copy 7. Appends fallback script to `` +### Optional Libraries (Chart.js & Flatpickr) + +Optional libraries are loaded on-demand using Jinja macros. This keeps page load times fast by only loading what's needed. + +**Location:** `app/templates/shared/includes/optional-libs.html` + +#### Loading Chart.js + +In your page template: + +```jinja +{% block chartjs_script %} +{% from 'shared/includes/optional-libs.html' import chartjs_loader %} +{{ chartjs_loader() }} +{% endblock %} +``` + +**Generated code:** + +```html + +``` + +#### Loading Flatpickr + +Flatpickr requires both CSS and JS: + +```jinja +{# In head - load CSS #} +{% block flatpickr_css %} +{% from 'shared/includes/optional-libs.html' import flatpickr_css_loader %} +{{ flatpickr_css_loader() }} +{% endblock %} + +{# Before page scripts - load JS #} +{% block flatpickr_script %} +{% from 'shared/includes/optional-libs.html' import flatpickr_loader %} +{{ flatpickr_loader() }} +{% endblock %} +``` + +**Generated CSS code:** + +```html + +``` + +#### Available Blocks in admin/base.html + +| Block | Purpose | Location | +|-------|---------|----------| +| `flatpickr_css` | Flatpickr CSS | In `` | +| `chartjs_script` | Chart.js | Before page scripts | +| `flatpickr_script` | Flatpickr JS | Before page scripts | +| `extra_scripts` | Page-specific JS | Last in body | + ## File Structure ``` static/ ├── shared/ │ ├── css/ -│ │ └── tailwind.min.css # 2.9M - Tailwind CSS v2.2.19 +│ │ ├── tailwind.min.css # 2.9M - Tailwind CSS v2.2.19 +│ │ └── vendor/ +│ │ └── flatpickr.min.css # 16K - Flatpickr v4.6.13 │ └── js/ │ └── vendor/ -│ └── alpine.min.js # 44K - Alpine.js v3.13.3 +│ ├── alpine.min.js # 44K - Alpine.js v3.13.3 +│ ├── chart.umd.min.js # 205K - Chart.js v4.4.1 +│ └── flatpickr.min.js # 51K - Flatpickr v4.6.13 ├── shop/ │ └── css/ -│ └── shop.css # Shop-specific styles +│ └── shop.css # Shop-specific styles ├── vendor/ │ └── css/ -│ └── tailwind.output.css # Vendor-specific overrides +│ └── tailwind.output.css # Vendor-specific overrides └── admin/ └── css/ - └── tailwind.output.css # Admin-specific overrides + └── tailwind.output.css # Admin-specific overrides + +app/templates/shared/ +├── includes/ +│ └── optional-libs.html # CDN fallback loaders for Chart.js & Flatpickr +└── macros/ + ├── charts.html # Chart.js wrapper macros + └── datepicker.html # Flatpickr wrapper macros ``` ## Frontend-Specific Implementations @@ -217,7 +310,33 @@ cd static/shared/js/vendor curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js ``` -**Important:** Update both the local file AND the CDN URL in all three base templates. +To update Chart.js: + +```bash +cd static/shared/js/vendor +curl -o chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js +``` + +To update Flatpickr: + +```bash +# JavaScript +cd static/shared/js/vendor +curl -o flatpickr.min.js https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js + +# CSS +cd static/shared/css/vendor +curl -o flatpickr.min.css https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css +``` + +**Important:** Update both the local file AND the CDN URL in the following locations: + +| Asset | Update Location | +|-------|-----------------| +| Tailwind CSS | All three `base.html` templates | +| Alpine.js | All three `base.html` templates | +| Chart.js | `shared/includes/optional-libs.html` | +| Flatpickr | `shared/includes/optional-libs.html` | ## Production Deployment @@ -226,6 +345,8 @@ curl -o alpine.min.js https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min. Before deploying to production, verify: - [ ] Local asset files exist in `static/shared/` +- [ ] Core assets: `tailwind.min.css`, `alpine.min.js` +- [ ] Optional assets: `chart.umd.min.js`, `flatpickr.min.js`, `flatpickr.min.css` - [ ] File permissions are correct (readable by web server) - [ ] Static file serving is enabled in FastAPI - [ ] CDN URLs are accessible from production network @@ -239,9 +360,14 @@ Ensure local assets are included in Docker image: # Dockerfile COPY static/ /app/static/ -# Verify files exist +# Verify core files exist RUN test -f /app/static/shared/css/tailwind.min.css && \ test -f /app/static/shared/js/vendor/alpine.min.js + +# Verify optional library files exist +RUN test -f /app/static/shared/js/vendor/chart.umd.min.js && \ + test -f /app/static/shared/js/vendor/flatpickr.min.js && \ + test -f /app/static/shared/css/vendor/flatpickr.min.css ``` ### Static File Configuration diff --git a/docs/frontend/shared/jinja-macros.md b/docs/frontend/shared/jinja-macros.md new file mode 100644 index 00000000..2fca4210 --- /dev/null +++ b/docs/frontend/shared/jinja-macros.md @@ -0,0 +1,1134 @@ +# Jinja Macros Library + +**Version:** 1.0 +**Last Updated:** December 2024 +**Location:** `app/templates/shared/macros/` + +--- + +## Overview + +The Jinja macros library provides reusable UI components for building consistent admin pages. All macros are built with: + +- **Tailwind CSS** for styling +- **Alpine.js** for interactivity +- **Dark mode** support built-in +- **Accessibility** best practices + +### Quick Start + +```jinja +{% from 'shared/macros/buttons.html' import btn_primary %} +{% from 'shared/macros/forms.html' import form_input %} +{% from 'shared/macros/modals.html' import confirm_modal %} + +{{ btn_primary('Save Changes', onclick='saveForm()') }} +{{ form_input('Email', 'email', 'formData.email', type='email', required=true) }} +{{ confirm_modal('deleteModal', 'Delete Item', 'Are you sure?', 'deleteItem()', 'showDeleteModal') }} +``` + +--- + +## Available Macro Files + +| File | Macros | Description | +|------|--------|-------------| +| [alerts.html](#alerts) | 5 | Loading states, error alerts, toasts | +| [avatars.html](#avatars) | 7 | User avatars with status indicators | +| [badges.html](#badges) | 8 | Status badges, role badges, counts | +| [buttons.html](#buttons) | 11 | Primary, secondary, danger, icon buttons | +| [cards.html](#cards) | 7 | Stats cards, info cards, filter cards | +| [charts.html](#charts) | 7 | Chart.js integration with Card wrappers | +| [datepicker.html](#datepicker) | 6 | Flatpickr date/time pickers | +| [dropdowns.html](#dropdowns) | 8 | Dropdown menus, context menus | +| [forms.html](#forms) | 12 | Input fields, selects, checkboxes | +| [headers.html](#headers) | 6 | Page headers, breadcrumbs, tabs | +| [modals.html](#modals) | 5 | Modal dialogs, slide-overs | +| [pagination.html](#pagination) | 2 | Table pagination controls | +| [tables.html](#tables) | 10 | Table wrappers, cells, empty states | + +**Total: 94 macros** + +--- + +## Alerts + +**File:** `shared/macros/alerts.html` + +```jinja +{% from 'shared/macros/alerts.html' import loading_state, error_state, alert, toast %} +``` + +### loading_state + +Shows a centered loading spinner with message. + +```jinja +{{ loading_state(show_condition='loading', message='Loading data...') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `show_condition` | string | `'loading'` | Alpine.js condition | +| `message` | string | `'Loading...'` | Loading message | + +### error_state + +Shows an error alert with icon. + +```jinja +{{ error_state(show_condition='error', error_var='errorMessage') }} +``` + +### alert + +Static alert box with variants. + +```jinja +{{ alert('Operation completed successfully!', variant='success', dismissible=true) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `message` | string | required | Alert message | +| `variant` | string | `'info'` | `'success'`, `'warning'`, `'error'`, `'info'` | +| `dismissible` | bool | `false` | Show close button | +| `icon` | string | auto | Icon name | + +### toast + +Toast notification (auto-dismiss). + +```jinja +{{ toast('Item saved!', 'success') }} +``` + +--- + +## Avatars + +**File:** `shared/macros/avatars.html` + +```jinja +{% from 'shared/macros/avatars.html' import avatar, avatar_with_status, avatar_initials, avatar_group %} +``` + +### avatar + +Basic avatar component. + +```jinja +{# Static image #} +{{ avatar(src='/images/user.jpg', alt='John Doe', size='lg') }} + +{# Dynamic with Alpine.js #} +{{ avatar(src='user.avatar_url', alt='user.name', size='md', dynamic=true) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `src` | string | `''` | Image URL or Alpine.js expression | +| `alt` | string | `''` | Alt text | +| `size` | string | `'md'` | `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'` | +| `dynamic` | bool | `false` | Whether src is Alpine.js expression | +| `fallback_icon` | string | `'user'` | Icon when no image | + +**Sizes:** +- `xs`: 24px +- `sm`: 32px +- `md`: 40px +- `lg`: 48px +- `xl`: 56px +- `2xl`: 64px + +### avatar_with_status + +Avatar with online/offline indicator. + +```jinja +{{ avatar_with_status(src='user.avatar', status='online', size='md', dynamic=true) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `status` | string | `'online'` | `'online'`, `'offline'`, `'away'`, `'busy'` | + +### avatar_initials + +Avatar showing initials. + +```jinja +{{ avatar_initials(initials='JD', size='md', color='purple') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `initials` | string | required | 1-2 characters | +| `color` | string | `'purple'` | `'gray'`, `'purple'`, `'blue'`, `'green'`, `'red'`, `'yellow'`, `'orange'` | + +### avatar_group + +Stacked avatar group. + +```jinja +{% call avatar_group(max=4, total_var='team.length', size='sm') %} + +{% endcall %} +``` + +### user_avatar_card + +Avatar with name and subtitle. + +```jinja +{{ user_avatar_card(src='user.avatar', name='user.name', subtitle='user.role', size='md') }} +``` + +--- + +## Badges + +**File:** `shared/macros/badges.html` + +```jinja +{% from 'shared/macros/badges.html' import badge, status_badge, role_badge, severity_badge %} +``` + +### badge + +Generic badge. + +```jinja +{{ badge('New', variant='success', icon='check') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `text` | string | required | Badge text | +| `variant` | string | `'gray'` | `'gray'`, `'green'`, `'red'`, `'yellow'`, `'blue'`, `'purple'` | +| `icon` | string | `none` | Optional icon | +| `size` | string | `'md'` | `'sm'`, `'md'` | + +### status_badge + +Dynamic status badge based on boolean. + +```jinja +{{ status_badge(condition='item.is_active', true_text='Active', false_text='Inactive') }} +``` + +### role_badge + +User role badge. + +```jinja +{{ role_badge(role='item.role') }} +``` + +Automatically colors: admin=red, manager=purple, staff=blue, viewer=gray + +### severity_badge + +Severity level badge. + +```jinja +{{ severity_badge(severity='violation.severity') }} +``` + +Levels: critical=red, error=orange, warning=yellow, info=blue + +### order_status_badge + +Order status badge. + +```jinja +{{ order_status_badge(status='order.status') }} +``` + +Statuses: pending, processing, shipped, delivered, cancelled, refunded + +### count_badge + +Numeric count badge (notification style). + +```jinja +{{ count_badge(count='notifications.length', max=99) }} +``` + +--- + +## Buttons + +**File:** `shared/macros/buttons.html` + +```jinja +{% from 'shared/macros/buttons.html' import btn, btn_primary, btn_secondary, btn_danger, action_button %} +``` + +### btn + +Base button macro. + +```jinja +{{ btn('Click Me', variant='primary', size='md', icon='plus', onclick='doSomething()') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `label` | string | required | Button text | +| `variant` | string | `'primary'` | `'primary'`, `'secondary'`, `'danger'`, `'success'`, `'ghost'` | +| `size` | string | `'md'` | `'sm'`, `'md'`, `'lg'` | +| `icon` | string | `none` | Icon name | +| `icon_position` | string | `'left'` | `'left'`, `'right'` | +| `onclick` | string | `none` | Alpine.js click handler | +| `href` | string | `none` | Makes it a link | +| `disabled` | string | `none` | Alpine.js disabled condition | +| `loading` | string | `none` | Alpine.js loading condition | +| `type` | string | `'button'` | Button type | + +### Convenience Variants + +```jinja +{{ btn_primary('Save', icon='check', onclick='save()') }} +{{ btn_secondary('Cancel', onclick='cancel()') }} +{{ btn_danger('Delete', icon='trash', onclick='delete()') }} +{{ btn_success('Approve', onclick='approve()') }} +``` + +### action_button + +Icon-only action button for tables. + +```jinja +{{ action_button(icon='edit', onclick='edit(item)', variant='primary', title='Edit') }} +{{ action_button(icon='trash', onclick='delete(item)', variant='danger', title='Delete') }} +``` + +### action_button_group + +Group of action buttons. + +```jinja +{% call action_button_group() %} + {{ action_button(icon='eye', href="'/items/' + item.id", variant='info', title='View') }} + {{ action_button(icon='edit', onclick='edit(item)', variant='primary', title='Edit') }} + {{ action_button(icon='trash', onclick='confirmDelete(item)', variant='danger', title='Delete') }} +{% endcall %} +``` + +### back_button + +Back navigation button. + +```jinja +{{ back_button(href='/admin/items', label='Back to List') }} +``` + +### submit_button + +Form submit button with loading state. + +```jinja +{{ submit_button(label='Save Changes', loading_var='saving', loading_text='Saving...') }} +``` + +--- + +## Cards + +**File:** `shared/macros/cards.html` + +```jinja +{% from 'shared/macros/cards.html' import stat_card, card, info_card, filter_card %} +``` + +### stat_card + +Statistics card with icon and value. + +```jinja +{{ stat_card(icon='users', label='Total Users', value='stats.totalUsers', color='purple') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `icon` | string | required | Icon name | +| `label` | string | required | Stat label | +| `value` | string | required | Alpine.js expression | +| `color` | string | `'orange'` | `'orange'`, `'green'`, `'blue'`, `'purple'`, `'red'`, `'yellow'`, `'teal'` | +| `format` | string | `none` | Format function (e.g., `'formatCurrency'`) | + +### stats_grid + +Grid container for stat cards. + +```jinja +{% call stats_grid(columns=4) %} + {{ stat_card(icon='users', label='Users', value='stats.users', color='blue') }} + {{ stat_card(icon='shopping-cart', label='Orders', value='stats.orders', color='green') }} + {{ stat_card(icon='currency-dollar', label='Revenue', value='stats.revenue', color='purple', format='formatCurrency') }} + {{ stat_card(icon='chart-bar', label='Growth', value='stats.growth', color='orange') }} +{% endcall %} +``` + +### card + +Basic card container. + +```jinja +{% call card(title='User Details', subtitle='View and edit user information') %} +

Card content here

+{% endcall %} +``` + +### info_card + +Card with icon and description. + +```jinja +{{ info_card(icon='document', title='Documentation', description='View the full docs', color='blue', href='/docs') }} +``` + +### filter_card + +Card for search and filter controls. + +```jinja +{% call filter_card() %} + {{ search_input(x_model='filters.search', on_input='debouncedSearch()') }} + {{ filter_select(x_model='filters.status', options=[...]) }} +{% endcall %} +``` + +--- + +## Charts + +**File:** `shared/macros/charts.html` + +**Prerequisites:** Requires Chart.js. See [CDN Fallback Strategy](../cdn-fallback-strategy.md). + +```jinja +{% from 'shared/macros/charts.html' import chart_card, line_chart, bar_chart, doughnut_chart, chart_config_script %} +``` + +### Loading Chart.js + +In your page template: + +```jinja +{% block chartjs_script %} +{% from 'shared/includes/optional-libs.html' import chartjs_loader %} +{{ chartjs_loader() }} +{% endblock %} +``` + +### chart_card + +Chart in a card with title and optional menu. + +```jinja +{{ chart_card('salesChart', 'Monthly Sales', 'line', height='300px') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `id` | string | required | Canvas element ID | +| `title` | string | required | Card title | +| `chart_type` | string | `'line'` | `'line'`, `'bar'`, `'doughnut'`, `'pie'` | +| `height` | string | `'300px'` | Chart height | +| `show_menu` | bool | `true` | Show dropdown menu | + +### Standalone Charts + +```jinja +{{ line_chart('revenueChart', height='250px') }} +{{ bar_chart('ordersChart', height='300px') }} +{{ doughnut_chart('categoryChart', size='200px') }} +``` + +### chart_config_script + +Include Chart.js configuration helpers. + +```jinja +{{ chart_config_script() }} + + +``` + +**Available helper functions:** +- `createChart(id, type, data, options)` +- `createLineChart(id, labels, datasets, options)` +- `createBarChart(id, labels, datasets, options)` +- `createDoughnutChart(id, labels, data, colors, options)` + +**Color presets:** `chartColors.purple`, `chartColors.blue`, `chartColors.green`, `chartColors.red`, `chartColors.yellow`, `chartColors.gray` + +--- + +## Datepicker + +**File:** `shared/macros/datepicker.html` + +**Prerequisites:** Requires Flatpickr. See [CDN Fallback Strategy](../cdn-fallback-strategy.md). + +```jinja +{% from 'shared/macros/datepicker.html' import datepicker, daterange_picker, datetime_picker, time_picker %} +``` + +### Loading Flatpickr + +In your page template: + +```jinja +{% block flatpickr_css %} +{% from 'shared/includes/optional-libs.html' import flatpickr_css_loader %} +{{ flatpickr_css_loader() }} +{% endblock %} + +{% block flatpickr_script %} +{% from 'shared/includes/optional-libs.html' import flatpickr_loader %} +{{ flatpickr_loader() }} +{% endblock %} +``` + +### datepicker + +Single date picker. + +```jinja +{{ datepicker('startDate', 'formData.startDate', label='Start Date', required=true) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `id` | string | required | Input ID | +| `x_model` | string | required | Alpine.js model | +| `label` | string | `none` | Field label | +| `placeholder` | string | `'Select date'` | Placeholder text | +| `format` | string | `'Y-m-d'` | Date format | +| `min_date` | string | `none` | Minimum date | +| `max_date` | string | `none` | Maximum date | +| `required` | bool | `false` | Required field | + +### daterange_picker + +Date range picker. + +```jinja +{{ daterange_picker('dateRange', 'filters.dateRange', label='Date Range') }} +``` + +### datetime_picker + +Date and time picker. + +```jinja +{{ datetime_picker('scheduledAt', 'formData.scheduledAt', label='Schedule For', minute_increment=15) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `time_24hr` | bool | `true` | Use 24-hour format | +| `minute_increment` | int | `5` | Minute step | + +### time_picker + +Time-only picker. + +```jinja +{{ time_picker('startTime', 'formData.startTime', label='Start Time') }} +``` + +--- + +## Dropdowns + +**File:** `shared/macros/dropdowns.html` + +```jinja +{% from 'shared/macros/dropdowns.html' import dropdown, context_menu, dropdown_item, dropdown_divider %} +``` + +### dropdown + +Button with dropdown menu. + +```jinja +{% call dropdown('Actions', position='right') %} + {{ dropdown_item('Edit', onclick='edit()', icon='pencil') }} + {{ dropdown_item('Duplicate', onclick='duplicate()', icon='document-duplicate') }} + {{ dropdown_divider() }} + {{ dropdown_item('Delete', onclick='delete()', icon='trash', variant='danger') }} +{% endcall %} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `label` | string | required | Button label | +| `position` | string | `'right'` | `'left'`, `'right'` | +| `variant` | string | `'secondary'` | `'primary'`, `'secondary'`, `'ghost'` | +| `icon` | string | `'chevron-down'` | Button icon | +| `width` | string | `'w-48'` | Dropdown width | + +### context_menu + +Three-dot context menu (for table rows). + +```jinja +{% call context_menu() %} + {{ dropdown_item('View', href="'/items/' + item.id", icon='eye') }} + {{ dropdown_item('Edit', onclick='edit(item)', icon='pencil') }} + {{ dropdown_divider() }} + {{ dropdown_item('Delete', onclick='confirmDelete(item)', icon='trash', variant='danger') }} +{% endcall %} +``` + +### dropdown_item + +Menu item. + +```jinja +{{ dropdown_item('Edit', onclick='edit()', icon='pencil') }} +{{ dropdown_item('View Details', href='/details/123', icon='eye') }} +{{ dropdown_item('Delete', onclick='delete()', icon='trash', variant='danger') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `label` | string | required | Item label | +| `onclick` | string | `none` | Click handler | +| `href` | string | `none` | Link URL | +| `icon` | string | `none` | Icon name | +| `variant` | string | `'default'` | `'default'`, `'danger'` | +| `disabled` | bool | `false` | Disabled state | + +### select_dropdown + +Custom select dropdown. + +```jinja +{% call select_dropdown(placeholder='Select status...') %} + {{ select_option('active', 'Active') }} + {{ select_option('inactive', 'Inactive') }} + {{ select_option('pending', 'Pending') }} +{% endcall %} +``` + +--- + +## Forms + +**File:** `shared/macros/forms.html` + +```jinja +{% from 'shared/macros/forms.html' import form_input, form_select, form_textarea, form_checkbox, form_toggle, password_input, search_input %} +``` + +### form_input + +Standard text input. + +```jinja +{{ form_input('Email', 'email', 'formData.email', type='email', required=true, placeholder='Enter email') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `label` | string | required | Field label | +| `name` | string | required | Input name | +| `x_model` | string | required | Alpine.js model | +| `type` | string | `'text'` | Input type | +| `placeholder` | string | `''` | Placeholder | +| `required` | bool | `false` | Required | +| `disabled` | string | `none` | Alpine.js disabled condition | +| `error` | string | `none` | Alpine.js error message | +| `help` | string | `none` | Help text | +| `maxlength` | int | `none` | Max length | + +### password_input + +Password input with show/hide toggle. + +```jinja +{{ password_input('Password', 'formData.password', required=true, show_strength=true) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `show_strength` | bool | `false` | Show strength indicator | +| `minlength` | int | `none` | Minimum length | + +### input_with_icon + +Input with icon. + +```jinja +{{ input_with_icon('Website', 'formData.url', 'url', icon='globe', placeholder='https://...') }} +{{ input_with_icon('Search', 'query', 'q', icon='search', on_click_icon='search()') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `icon` | string | required | Icon name | +| `icon_position` | string | `'left'` | `'left'`, `'right'` | +| `on_click_icon` | string | `none` | Makes icon clickable | + +### form_select + +Select dropdown. + +```jinja +{{ form_select('Status', 'formData.status', [ + {'value': 'active', 'label': 'Active'}, + {'value': 'inactive', 'label': 'Inactive'} +], required=true) }} +``` + +### form_select_dynamic + +Select with Alpine.js options. + +```jinja +{{ form_select_dynamic('Category', 'formData.categoryId', 'categories', value_key='id', label_key='name') }} +``` + +### form_textarea + +Textarea input. + +```jinja +{{ form_textarea('Description', 'formData.description', rows=4, maxlength=500) }} +``` + +### form_checkbox + +Checkbox input. + +```jinja +{{ form_checkbox('Subscribe to newsletter', 'formData.subscribe', help='We will send weekly updates') }} +``` + +### form_toggle + +Toggle switch. + +```jinja +{{ form_toggle('Enable notifications', 'formData.notifications') }} +``` + +### form_radio_group + +Radio button group. + +```jinja +{{ form_radio_group('Priority', 'priority', 'formData.priority', [ + {'value': 'low', 'label': 'Low'}, + {'value': 'medium', 'label': 'Medium'}, + {'value': 'high', 'label': 'High'} +], inline=true) }} +``` + +### search_input + +Search input with icon. + +```jinja +{{ search_input(x_model='filters.search', placeholder='Search items...', on_input='debouncedSearch()') }} +``` + +### filter_select + +Compact filter select (no label). + +```jinja +{{ filter_select(x_model='filters.status', options=[ + {'value': 'active', 'label': 'Active'}, + {'value': 'inactive', 'label': 'Inactive'} +], placeholder='All Statuses', on_change='applyFilters()') }} +``` + +### file_input + +Drag and drop file upload. + +```jinja +{{ file_input('Upload Image', 'image', accept='image/*', max_size=5, on_change='handleFileSelect($event)') }} +``` + +--- + +## Headers + +**File:** `shared/macros/headers.html` + +```jinja +{% from 'shared/macros/headers.html' import page_header, section_header, breadcrumbs, tab_header %} +``` + +### page_header + +Page title with optional action button. + +```jinja +{{ page_header('User Management', subtitle='Manage all users', action_label='Add User', action_url='/admin/users/create', action_icon='plus') }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `title` | string | required | Page title | +| `subtitle` | string | `none` | Subtitle | +| `action_label` | string | `none` | Action button text | +| `action_url` | string | `none` | Action button URL | +| `action_onclick` | string | `none` | Action click handler | +| `action_icon` | string | `'plus'` | Action button icon | +| `back_url` | string | `none` | Back button URL | + +### page_header_dynamic + +Page header with Alpine.js title. + +```jinja +{{ page_header_dynamic(title_var='pageTitle', back_url='/admin/items') }} +``` + +### section_header + +Section header within a page. + +```jinja +{{ section_header('Account Settings', subtitle='Update your account preferences', icon='cog', action_label='Edit', action_onclick='openSettings()') }} +``` + +### breadcrumbs + +Navigation breadcrumbs. + +```jinja +{{ breadcrumbs([ + {'label': 'Home', 'url': '/admin'}, + {'label': 'Users', 'url': '/admin/users'}, + {'label': 'John Doe'} +]) }} +``` + +### tab_header + +Tab navigation. + +```jinja +{{ tab_header([ + {'id': 'general', 'label': 'General', 'icon': 'cog'}, + {'id': 'security', 'label': 'Security', 'icon': 'shield-check'}, + {'id': 'notifications', 'label': 'Notifications', 'icon': 'bell'} +], active_var='activeTab') }} +``` + +--- + +## Modals + +**File:** `shared/macros/modals.html` + +```jinja +{% from 'shared/macros/modals.html' import modal, confirm_modal, form_modal, slide_over %} +``` + +### modal + +Basic modal dialog. + +```jinja +{% call modal('editModal', 'Edit Item', show_var='isEditModalOpen', size='lg') %} +

Modal content here

+{% endcall %} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `id` | string | required | Modal ID | +| `title` | string | required | Modal title | +| `show_var` | string | `'isModalOpen'` | Alpine.js visibility variable | +| `size` | string | `'md'` | `'sm'`, `'md'`, `'lg'`, `'xl'`, `'full'` | +| `show_close` | bool | `true` | Show close button | +| `show_footer` | bool | `true` | Show footer | +| `close_on_backdrop` | bool | `true` | Close on backdrop click | +| `close_on_escape` | bool | `true` | Close on Escape key | + +### confirm_modal + +Confirmation dialog for destructive actions. + +```jinja +{{ confirm_modal( + 'deleteModal', + 'Delete User', + 'Are you sure you want to delete this user? This action cannot be undone.', + 'deleteUser()', + 'isDeleteModalOpen', + confirm_text='Delete', + variant='danger' +) }} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `confirm_action` | string | required | Alpine.js action on confirm | +| `confirm_text` | string | `'Confirm'` | Confirm button text | +| `cancel_text` | string | `'Cancel'` | Cancel button text | +| `variant` | string | `'danger'` | `'danger'`, `'warning'`, `'info'` | + +### form_modal + +Modal optimized for forms with loading state. + +```jinja +{% call form_modal('createModal', 'Create Item', submit_action='createItem()', loading_var='saving') %} + {{ form_input('Name', 'name', 'formData.name', required=true) }} + {{ form_textarea('Description', 'formData.description') }} +{% endcall %} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `submit_action` | string | `'submitForm()'` | Form submit handler | +| `submit_text` | string | `'Save'` | Submit button text | +| `loading_var` | string | `'saving'` | Loading state variable | +| `loading_text` | string | `'Saving...'` | Loading button text | + +### slide_over + +Side panel that slides in from the right. + +```jinja +{% call slide_over('detailsPanel', 'Item Details', show_var='isPanelOpen', width='lg') %} +
+

Panel content here

+
+{% endcall %} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `width` | string | `'md'` | `'sm'`, `'md'`, `'lg'`, `'xl'` | + +--- + +## Pagination + +**File:** `shared/macros/pagination.html` + +```jinja +{% from 'shared/macros/pagination.html' import pagination, pagination_simple %} +``` + +### pagination + +Full pagination with page numbers. + +```jinja +{{ pagination(show_condition='items.length > 0') }} +``` + +**Required Alpine.js properties:** +- `pagination.page` - Current page +- `pagination.total` - Total items +- `startIndex` - First item index +- `endIndex` - Last item index +- `totalPages` - Total pages +- `pageNumbers` - Array with page numbers and `'...'` + +**Required methods:** +- `previousPage()` +- `nextPage()` +- `goToPage(pageNum)` + +### pagination_simple + +Simple prev/next pagination. + +```jinja +{{ pagination_simple() }} +``` + +--- + +## Tables + +**File:** `shared/macros/tables.html` + +```jinja +{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_body, table_empty_state, table_cell_avatar, table_cell_date %} +``` + +### table_wrapper + +Table container with overflow handling. + +```jinja +{% call table_wrapper() %} + {{ table_header(['Name', 'Email', 'Status', 'Actions']) }} + + + +{% endcall %} +``` + +### table_header + +Table header row. + +```jinja +{{ table_header(['Name', 'Email', 'Role', 'Status', 'Created', 'Actions']) }} +``` + +### table_body + +Styled tbody wrapper. + +```jinja +{% call table_body() %} + +{% endcall %} +``` + +### table_empty_state + +Empty state message. + +```jinja +{{ table_empty_state(colspan=6, icon='inbox', title='No users found', message='Create your first user to get started') }} +``` + +### table_cell_avatar + +Cell with avatar and text. + +```jinja +{{ table_cell_avatar(image_src='item.avatar', title='item.name', subtitle='item.email') }} +``` + +### table_cell_text + +Simple text cell. + +```jinja +{{ table_cell_text(text='item.email', is_dynamic=true, truncate=true) }} +``` + +### table_cell_date + +Formatted date cell. + +```jinja +{{ table_cell_date(date_var='item.created_at') }} +``` + +### table_loading_overlay + +Loading overlay for table. + +```jinja +{{ table_loading_overlay(show_condition='loading') }} +``` + +--- + +## Complete Page Example + +```jinja +{% extends "admin/base.html" %} + +{% from 'shared/macros/headers.html' import page_header, breadcrumbs %} +{% from 'shared/macros/cards.html' import stat_card, stats_grid, filter_card %} +{% from 'shared/macros/forms.html' import search_input, filter_select %} +{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_body, table_empty_state, table_cell_avatar %} +{% from 'shared/macros/badges.html' import status_badge, role_badge %} +{% from 'shared/macros/buttons.html' import action_button %} +{% from 'shared/macros/pagination.html' import pagination %} +{% from 'shared/macros/modals.html' import confirm_modal %} +{% from 'shared/macros/alerts.html' import loading_state, error_state %} + +{% block content %} +{{ breadcrumbs([{'label': 'Admin', 'url': '/admin'}, {'label': 'Users'}]) }} +{{ page_header('User Management', action_label='Add User', action_url='/admin/users/create') }} + +{# Stats #} +{% call stats_grid() %} + {{ stat_card('users', 'Total Users', 'stats.total', 'blue') }} + {{ stat_card('check-circle', 'Active', 'stats.active', 'green') }} + {{ stat_card('x-circle', 'Inactive', 'stats.inactive', 'red') }} + {{ stat_card('shield-check', 'Admins', 'stats.admins', 'purple') }} +{% endcall %} + +{# Filters #} +{% call filter_card() %} + {{ search_input(x_model='filters.search', on_input='debouncedSearch()') }} + {{ filter_select(x_model='filters.status', options=[ + {'value': 'active', 'label': 'Active'}, + {'value': 'inactive', 'label': 'Inactive'} + ], on_change='loadItems()') }} +{% endcall %} + +{# Loading/Error States #} +{{ loading_state() }} +{{ error_state() }} + +{# Table #} +
+ {% call table_wrapper() %} + {{ table_header(['User', 'Role', 'Status', 'Created', 'Actions']) }} + {% call table_body() %} + {{ table_empty_state(5, show_condition='items.length === 0') }} + + {% endcall %} + {% endcall %} + {{ pagination() }} +
+ +{# Delete Confirmation Modal #} +{{ confirm_modal('deleteModal', 'Delete User', 'Are you sure you want to delete this user?', 'deleteUser()', 'showDeleteModal') }} +{% endblock %} +``` + +--- + +## Related Documentation + +- [UI Components (HTML Reference)](ui-components.md) +- [Pagination Guide](pagination.md) +- [CDN Fallback Strategy](../cdn-fallback-strategy.md) +- [Icons Guide](../../development/icons-guide.md) diff --git a/docs/frontend/shared/ui-components.md b/docs/frontend/shared/ui-components.md index 0502a720..3e48bf1d 100644 --- a/docs/frontend/shared/ui-components.md +++ b/docs/frontend/shared/ui-components.md @@ -12,6 +12,22 @@ The admin panel uses a consistent set of UI components built with Tailwind CSS a **Live Component Library:** Visit `/admin/components` in the admin panel to see all components with copy-paste ready code. +!!! tip "Use Jinja Macros Instead" + For new development, prefer using the **[Jinja Macros Library](jinja-macros.md)** instead of copy-pasting HTML. Macros provide: + + - **Consistency:** Same styling across all pages + - **Maintainability:** Update once, apply everywhere + - **Less code:** Parameters instead of full HTML + - **Type safety:** Built-in Alpine.js bindings + + ```jinja + {# Instead of 20 lines of HTML... #} + {% from 'shared/macros/buttons.html' import btn_primary %} + {{ btn_primary('Save Changes', icon='check', onclick='save()') }} + ``` + + See [Jinja Macros Library](jinja-macros.md) for full documentation. + --- ## Page Layout Structure @@ -462,6 +478,10 @@ function adminResourceList() { ## Related Documentation +- **[Jinja Macros Library](jinja-macros.md)** - Reusable macro components (recommended) +- [Pagination Guide](pagination.md) +- [CDN Fallback Strategy](../cdn-fallback-strategy.md) - [Tailwind CSS](../tailwind-css.md) +- [Icons Guide](../../development/icons-guide.md) - [Tailwind CSS Official Docs](https://tailwindcss.com/docs) - [Alpine.js Official Docs](https://alpinejs.dev/) diff --git a/mkdocs.yml b/mkdocs.yml index 469673ae..0ec413f1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,6 +86,7 @@ nav: - CDN Fallback Strategy: frontend/cdn-fallback-strategy.md - Tailwind CSS Build: frontend/tailwind-css.md - Shared Components: + - Jinja Macros Library: frontend/shared/jinja-macros.md - UI Components: frontend/shared/ui-components.md - UI Components Quick Reference: frontend/shared/ui-components-quick-reference.md - Pagination: frontend/shared/pagination.md