docs: add details modal and log modal patterns to component library

- Add Details Modal (Table Layout) example to components page
  - Shows header with icon and status badge
  - Stats cards grid (imported, updated, errors, total)
  - Key-value table with icon-labeled rows
- Add Log Details Modal example with live demo
  - Level-based coloring (warning=yellow, error=red, critical=purple)
  - Message, exception, and stack trace sections
  - Copy-to-clipboard for stack traces
  - Both error and warning log demo buttons
- Update jinja-macros.md with Details Modal Pattern documentation
  - Document the pattern structure and key features
  - Link to components library for live examples
- Add Alpine.js state variables for new modal demos

🤖 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-11 17:52:36 +01:00
parent 2239522d79
commit a40c88dcea
4 changed files with 421 additions and 2 deletions

View File

@@ -1229,6 +1229,7 @@ html {
rating_model='demoNewReview.rating',
title_model='demoNewReview.title',
content_model='demoNewReview.content',
images_model='demoNewReview.images',
on_submit='demoSubmitReview()',
submitting_var='submittingReview'
) }}
@@ -2272,6 +2273,303 @@ goToPage(n) { if (n !== '...' && n >= 1 && n <= this.totalPages) { this.paginati
Copy Code
</button>
</div>
<!-- Details Modal Pattern -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Details Modal (Table Layout)</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Pattern for displaying record details with a header (icon + badge), table layout, and optional sections. Used for Job Details, Log Details, etc.
</p>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3">
<button
@click="showDetailsModal = true"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
View Details Modal
</button>
<!-- Details Modal Example -->
<div
x-show="showDetailsModal"
x-cloak
x-transition
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showDetailsModal = false">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden">
{# Modal Header with Icon and Badge #}
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<span class="text-green-600 dark:text-green-400" x-html="$icon('check-circle', 'w-6 h-6')"></span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Record Details</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">ID: 12345</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="px-3 py-1 text-sm font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">Completed</span>
<button @click="showDetailsModal = false" class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<span x-html="$icon('x', 'w-6 h-6')"></span>
</button>
</div>
</div>
</div>
{# Modal Body #}
<div class="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">
<div class="space-y-6">
{# Stats Cards Row #}
<div class="grid grid-cols-4 gap-3">
<div class="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-center">
<p class="text-2xl font-bold text-green-600 dark:text-green-400">150</p>
<p class="text-xs text-green-700 dark:text-green-300 mt-1">Imported</p>
</div>
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-center">
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400">25</p>
<p class="text-xs text-blue-700 dark:text-blue-300 mt-1">Updated</p>
</div>
<div class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg text-center">
<p class="text-2xl font-bold text-red-600 dark:text-red-400">2</p>
<p class="text-xs text-red-700 dark:text-red-300 mt-1">Errors</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg text-center">
<p class="text-2xl font-bold text-gray-600 dark:text-gray-300">177</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Total</p>
</div>
</div>
{# Details Table #}
<div class="overflow-hidden border border-gray-200 dark:border-gray-700 rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50 w-1/3">
<div class="flex items-center gap-2">
<span x-html="$icon('user', 'w-4 h-4')"></span>
Owner
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">Acme Corp</td>
</tr>
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center gap-2">
<span x-html="$icon('clock', 'w-4 h-4')"></span>
Created
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">Dec 11, 2024 14:30</td>
</tr>
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center gap-2">
<span x-html="$icon('globe', 'w-4 h-4')"></span>
Source
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-xs">api/products.csv</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{# Modal Footer #}
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30">
<div class="flex justify-end">
<button
@click="showDetailsModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 transition-colors">
Close
</button>
</div>
</div>
</div>
</div>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-3">
<strong>Usage:</strong> See <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">app/templates/shared/macros/modals.html</code><code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">job_details_modal</code> for full implementation.
</p>
</div>
<!-- Log Details Modal Pattern -->
<div class="mb-8">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Log Details Modal</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Specialized modal for displaying log entries with level-based styling, message section, exception display, and stack trace with copy functionality.
</p>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-3">
<div class="flex gap-2">
<button
@click="showErrorLogDemo()"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
View Error Log
</button>
<button
@click="showWarningLogDemo()"
class="px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-lg hover:bg-yellow-700">
View Warning Log
</button>
</div>
<!-- Log Details Modal Example -->
<div
x-show="showLogModal"
x-cloak
x-transition
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showLogModal = false">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
{# Modal Header with Level Badge #}
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div :class="{
'bg-yellow-100 dark:bg-yellow-900/30': exampleLog?.level === 'WARNING',
'bg-red-100 dark:bg-red-900/30': exampleLog?.level === 'ERROR',
'bg-purple-100 dark:bg-purple-900/30': exampleLog?.level === 'CRITICAL',
'bg-blue-100 dark:bg-blue-900/30': exampleLog?.level === 'INFO'
}" class="p-2 rounded-lg">
<span :class="{
'text-yellow-600 dark:text-yellow-400': exampleLog?.level === 'WARNING',
'text-red-600 dark:text-red-400': exampleLog?.level === 'ERROR',
'text-purple-600 dark:text-purple-400': exampleLog?.level === 'CRITICAL',
'text-blue-600 dark:text-blue-400': exampleLog?.level === 'INFO'
}" x-html="$icon(exampleLog?.level === 'WARNING' ? 'exclamation' : exampleLog?.level === 'CRITICAL' ? 'lightning-bolt' : exampleLog?.level === 'ERROR' ? 'x-circle' : 'information-circle', 'w-6 h-6')"></span>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Log Entry Details</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">ID: <span x-text="exampleLog?.id"></span></p>
</div>
</div>
<div class="flex items-center gap-3">
<span :class="{
'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100': exampleLog?.level === 'WARNING',
'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100': exampleLog?.level === 'ERROR',
'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100': exampleLog?.level === 'CRITICAL',
'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100': exampleLog?.level === 'INFO'
}" class="px-3 py-1 text-sm font-semibold rounded-full" x-text="exampleLog?.level"></span>
<button @click="showLogModal = false" class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<span x-html="$icon('x', 'w-6 h-6')"></span>
</button>
</div>
</div>
</div>
{# Modal Body #}
<div class="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
<div class="space-y-6">
{# Details Table #}
<div class="overflow-hidden border border-gray-200 dark:border-gray-700 rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50 w-1/4">
<div class="flex items-center gap-2">
<span x-html="$icon('clock', 'w-4 h-4')"></span>
Timestamp
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100" x-text="new Date(exampleLog?.timestamp).toLocaleString()"></td>
</tr>
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center gap-2">
<span x-html="$icon('tag', 'w-4 h-4')"></span>
Logger
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-xs" x-text="exampleLog?.logger_name"></code>
</td>
</tr>
<tr>
<td class="px-4 py-3 text-sm font-medium text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center gap-2">
<span x-html="$icon('cube', 'w-4 h-4')"></span>
Module
</div>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-xs" x-text="exampleLog?.module"></code>
</td>
</tr>
</tbody>
</table>
</div>
{# Message Section #}
<div>
<div class="flex items-center gap-2 mb-2">
<span class="text-gray-500 dark:text-gray-400" x-html="$icon('chat-alt', 'w-4 h-4')"></span>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Message</h4>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 rounded-lg p-4">
<p class="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-words" x-text="exampleLog?.message"></p>
</div>
</div>
{# Exception Section (conditional) #}
<div x-show="exampleLog?.exception_message" x-transition>
<div class="flex items-center gap-2 mb-2">
<span class="text-red-500 dark:text-red-400" x-html="$icon('exclamation-circle', 'w-4 h-4')"></span>
<h4 class="text-sm font-semibold text-red-700 dark:text-red-300">Exception</h4>
</div>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 p-1.5 bg-red-100 dark:bg-red-900/50 rounded">
<span class="text-red-600 dark:text-red-400" x-html="$icon('x-circle', 'w-5 h-5')"></span>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-red-800 dark:text-red-200" x-text="exampleLog?.exception_type"></p>
<p class="text-sm text-red-600 dark:text-red-300 mt-1 break-words" x-text="exampleLog?.exception_message"></p>
</div>
</div>
</div>
</div>
{# Stack Trace Section (conditional) #}
<div x-show="exampleLog?.stack_trace" x-transition>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-gray-500 dark:text-gray-400" x-html="$icon('code', 'w-4 h-4')"></span>
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Stack Trace</h4>
</div>
<button
@click="navigator.clipboard.writeText(exampleLog?.stack_trace); Utils.showToast('Stack trace copied!', 'success')"
class="text-xs text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300 flex items-center gap-1">
<span x-html="$icon('clipboard-copy', 'w-4 h-4')"></span>
Copy
</button>
</div>
<div class="bg-gray-900 dark:bg-gray-950 border border-gray-700 rounded-lg overflow-hidden">
<pre class="p-4 text-xs text-green-400 font-mono overflow-x-auto max-h-64 overflow-y-auto"><code x-text="exampleLog?.stack_trace"></code></pre>
</div>
</div>
</div>
</div>
{# Modal Footer #}
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30">
<div class="flex justify-end">
<button
@click="showLogModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 transition-colors">
Close
</button>
</div>
</div>
</div>
</div>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2 mb-2">
<strong>Usage:</strong> See <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">app/templates/admin/logs.html</code> for full implementation.
</p>
</div>
</div>
</section>

View File

@@ -43,7 +43,7 @@ The Jinja macros library provides reusable UI components for building consistent
| [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 |
| [modals.html](#modals) | 5 + patterns | Modal dialogs, slide-overs, detail patterns |
| [pagination.html](#pagination) | 2 | Table pagination controls |
| [tables.html](#tables) | 10 | Table wrappers, cells, empty states |
@@ -927,6 +927,93 @@ Side panel that slides in from the right.
|-----------|------|---------|-------------|
| `width` | string | `'md'` | `'sm'`, `'md'`, `'lg'`, `'xl'` |
### Details Modal Pattern (Inline)
For complex detail views (job details, log entries, etc.), use inline modals with this pattern structure:
**Structure:**
1. **Header** - Icon with status-based coloring, title, subtitle, status badge, close button
2. **Stats Cards** - Grid of colored stat cards (imported, updated, errors, total)
3. **Details Table** - Key-value pairs with icons in a bordered table
4. **Content Sections** - Message, exception, stack trace (conditional)
5. **Footer** - Close button with proper styling
**Example implementations:**
- Job Details Modal: `app/templates/shared/macros/modals.html``job_details_modal`
- Log Details Modal: `app/templates/admin/logs.html` (inline)
**Key Features:**
- Level-based icon and color theming (success=green, warning=yellow, error=red, critical=purple)
- Stats cards grid with color-coded backgrounds
- Table layout with icon-labeled rows
- Conditional sections (exception, stack trace) with `x-show`
- Copy-to-clipboard for stack traces
- Dark mode support throughout
```jinja
{# Example structure #}
<div x-show="selectedItem" class="fixed inset-0 z-50 ...">
<div class="bg-white dark:bg-gray-800 rounded-lg ...">
{# Header with Icon and Badge #}
<div class="px-6 py-4 border-b ...">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<span class="text-green-600" x-html="$icon('check-circle', 'w-6 h-6')"></span>
</div>
<div>
<h3 class="text-lg font-semibold">Title</h3>
<p class="text-sm text-gray-500">Subtitle</p>
</div>
</div>
<div class="flex items-center gap-3">
<span class="px-3 py-1 text-sm font-semibold rounded-full bg-green-100 text-green-800">Status</span>
<button @click="selectedItem = null">×</button>
</div>
</div>
</div>
{# Body: Stats Cards + Details Table + Content Sections #}
<div class="p-6 overflow-y-auto ...">
{# Stats Cards Grid #}
<div class="grid grid-cols-4 gap-3">
<div class="p-3 bg-green-50 rounded-lg text-center">
<p class="text-2xl font-bold text-green-600">150</p>
<p class="text-xs text-green-700">Imported</p>
</div>
<!-- More cards... -->
</div>
{# Details Table #}
<div class="overflow-hidden border rounded-lg">
<table class="min-w-full">
<tbody>
<tr>
<td class="px-4 py-3 bg-gray-50 w-1/3">
<div class="flex items-center gap-2">
<span x-html="$icon('user', 'w-4 h-4')"></span>
Field Name
</div>
</td>
<td class="px-4 py-3">Value</td>
</tr>
</tbody>
</table>
</div>
</div>
{# Footer #}
<div class="px-6 py-4 border-t bg-gray-50">
<div class="flex justify-end">
<button @click="selectedItem = null">Close</button>
</div>
</div>
</div>
</div>
```
See the [Components Library](/admin/components#modals) for live examples.
---
## Pagination

File diff suppressed because one or more lines are too long

View File

@@ -424,6 +424,40 @@ function adminComponents() {
// Modal state variables for examples
showExampleModal: false,
showFormModal: false,
showDetailsModal: false,
showLogModal: false,
exampleLog: null,
// Example log data for demo
showErrorLogDemo() {
this.exampleLog = {
id: 1,
level: 'ERROR',
timestamp: new Date().toISOString(),
logger_name: 'app.services.import',
module: 'import_service',
message: 'Failed to import product: Invalid GTIN format',
exception_type: 'ValidationError',
exception_message: 'GTIN must be 13 digits',
stack_trace: 'Traceback (most recent call last):\n File "/app/services/import.py", line 42, in process\n validate_gtin(product.gtin)\n File "/app/validators.py", line 15, in validate_gtin\n raise ValidationError("GTIN must be 13 digits")\nValidationError: GTIN must be 13 digits'
};
this.showLogModal = true;
},
showWarningLogDemo() {
this.exampleLog = {
id: 2,
level: 'WARNING',
timestamp: new Date().toISOString(),
logger_name: 'app.utils.cache',
module: 'cache',
message: 'Cache miss for key: product_list_vendor_5. Fetching from database.',
exception_type: null,
exception_message: null,
stack_trace: null
};
this.showLogModal = true;
},
// ✅ CRITICAL: Proper initialization with guard
async init() {