Merge pull request #373 from christianlouis/copilot/implement-responsive-mobile-interface

feat(ui): implement responsive mobile interface
This commit is contained in:
Christian Krakau-Louis
2026-02-23 20:14:19 +01:00
committed by GitHub
4 changed files with 132 additions and 41 deletions
+20
View File
@@ -16,3 +16,23 @@ body {
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
/* Ensure pagination wraps properly on small screens */
.pagination {
flex-wrap: wrap;
gap: 0.5rem;
}
.pagination-buttons {
flex-wrap: wrap;
}
/* Ensure filter items stack on very small screens */
@media (max-width: 480px) {
.filter-group {
flex-direction: column;
}
.filter-item {
min-width: unset;
width: 100%;
}
}
+13 -12
View File
@@ -23,7 +23,7 @@
<body class="bg-gray-50 min-h-screen flex flex-col">
<!-- Global Nav -->
<nav class="bg-white shadow">
<nav class="bg-white shadow relative">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between h-16 items-center">
<!-- Brand + Icon -->
@@ -104,7 +104,8 @@
<button
@click="mobileMenuOpen = !mobileMenuOpen"
type="button"
class="md:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
class="md:hidden inline-flex items-center justify-center p-3 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
style="min-height:44px;min-width:44px;"
:aria-expanded="mobileMenuOpen"
>
<span class="sr-only">Open main menu</span>
@@ -125,9 +126,9 @@
class="md:hidden absolute top-16 inset-x-0 bg-white shadow-md z-50"
>
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="/" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Home</a>
<a href="/upload" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Upload</a>
<a href="/files" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Files</a>
<a href="/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Home</a>
<a href="/upload" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Upload</a>
<a href="/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">Files</a>
<!-- Admin section in mobile menu (shown only for admin users via JS) -->
<div id="mobileAdminSection" class="hidden">
@@ -135,30 +136,30 @@
<p class="px-3 py-1 text-xs font-semibold text-red-600 uppercase tracking-wider flex items-center">
<i class="fas fa-shield-alt mr-1"></i> Admin
</p>
<a href="/settings" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<a href="/settings" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-cog mr-2 text-gray-400"></i> Settings
</a>
<a href="/admin/credentials" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<a href="/admin/credentials" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-key mr-2 text-yellow-400"></i> Credentials
</a>
<a href="/env" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<a href="/env" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-terminal mr-2 text-gray-400"></i> Environment
</a>
<a href="/admin/files" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<a href="/admin/files" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<i class="fas fa-folder-open mr-2 text-gray-400"></i> File Manager
</a>
</div>
</div>
<!-- Status (de-emphasized) in mobile -->
<a href="/status" class="block px-3 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-gray-600 hover:bg-gray-50">
<a href="/status" class="block px-3 py-3 rounded-md text-sm font-medium text-gray-400 hover:text-gray-600 hover:bg-gray-50">
<i class="fas fa-circle-dot mr-1"></i> Status
</a>
<a href="/about" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">About</a>
<a href="/about" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">About</a>
<!-- Mobile Auth Section -->
<div id="mobileAuthSection" class="block px-3 py-2 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<div id="mobileAuthSection" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
<!-- Will be populated by JS -->
</div>
</div>
+54 -18
View File
@@ -420,26 +420,26 @@
<!-- Bulk Actions Section -->
<div id="bulkActionsBar" class="filters-section" style="display: none; background-color: #e6f3ff;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3">
<div>
<strong id="selectedCount">0</strong> files selected
</div>
<div style="display: flex; gap: 1rem;">
<button type="button" onclick="bulkReprocess()" style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;">
<div class="flex flex-wrap gap-2">
<button type="button" onclick="bulkReprocess()" style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-sync"></i> Reprocess Selected
</button>
<button type="button" onclick="bulkDelete()" style="padding: 0.5rem 1rem; background-color: #e53e3e; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;">
<button type="button" onclick="bulkDelete()" style="padding: 0.5rem 1rem; background-color: #e53e3e; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-trash"></i> Delete Selected
</button>
<button type="button" onclick="clearSelection()" style="padding: 0.5rem 1rem; background-color: #718096; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600;">
<button type="button" onclick="clearSelection()" style="padding: 0.5rem 1rem; background-color: #718096; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
Clear Selection
</button>
</div>
</div>
</div>
<!-- File table -->
<div class="overflow-x-auto">
<!-- File table (desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="file-table" id="fileTable">
<thead>
<tr>
@@ -518,10 +518,10 @@
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else 'N/A' }}</td>
<td>
<div style="display: flex; align-items: center;">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file">
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -536,29 +536,65 @@
</table>
</div>
<!-- Mobile card view (visible on small screens only) -->
<div class="md:hidden space-y-3" id="mobileCardView">
{% for file in files %}
<div
class="bg-white rounded-lg shadow p-4 cursor-pointer"
role="button"
tabindex="0"
onclick="viewFileDetail({{ file.id }}, event)"
onkeydown="if(event.key==='Enter'||event.key===' '){viewFileDetail({{ file.id }}, event);}"
aria-label="View details for {{ file.original_filename }}"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<p class="font-semibold text-gray-800 truncate">{{ file.original_filename }}</p>
<p class="text-xs text-gray-500 mt-1">ID: {{ file.id }} &middot; {{ (file.file_size / 1024) | round(2) }} KB</p>
<p class="text-xs text-gray-500 mt-0.5">{{ file.mime_type }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else 'N/A' }}</p>
</div>
<div class="flex flex-col items-end gap-2 flex-shrink-0">
<span class="status-badge status-{{ file.processing_status }}">{{ file.processing_status | title }}</span>
<div class="flex gap-1" role="group" aria-label="File actions" onclick="event.stopPropagation();" onkeydown="event.stopPropagation();">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
{% else %}
<p class="text-center py-4 text-gray-500">No files found</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<div class="pagination">
<div class="pagination flex-wrap gap-2">
<div class="pagination-info">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files
</div>
<div class="pagination-buttons">
<div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %}
<button class="pagination-button" onclick="goToPage(1)">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})">Previous</button>
<button class="pagination-button" onclick="goToPage(1)" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" style="min-height:44px;">Previous</button>
{% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})">
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" style="min-height:44px;">
{{ p }}
</button>
{% endfor %}
{% if pagination.page < pagination.total_pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})">Last</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" style="min-height:44px;">Last</button>
{% endif %}
</div>
</div>
@@ -570,8 +606,8 @@
<div class="modal-title">Confirm Deletion</div>
<p>Are you sure you want to delete this file?</p>
<div class="modal-buttons">
<button id="cancelDelete" class="modal-btn modal-btn-cancel">Cancel</button>
<button id="confirmDelete" class="modal-btn modal-btn-delete">Delete</button>
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">Cancel</button>
<button id="confirmDelete" class="modal-btn modal-btn-delete" style="min-height:44px;min-width:80px;">Delete</button>
</div>
</div>
</div>
+45 -11
View File
@@ -2,22 +2,25 @@
{% block title %}Upload Document{% endblock %}
{% block content %}
<div class="flex flex-col items-center justify-center p-8">
<h1 class="text-3xl font-bold mb-8">Upload Files</h1>
<div class="flex flex-col items-center justify-center p-4 sm:p-8">
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8">Upload Files</h1>
<!-- File Upload Section -->
<div class="w-full max-w-2xl mb-8">
<h2 class="text-xl font-semibold mb-4">Upload from Computer</h2>
<div class="w-full max-w-2xl mb-6 sm:mb-8">
<h2 class="text-xl font-semibold mb-4">Upload from Device</h2>
<form action="/api/ui-upload" method="POST" enctype="multipart/form-data">
<div
id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-8 bg-white text-center w-full"
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
>
<p class="text-gray-500 mb-4">
Drag & drop files here, or click to select files.
<p class="text-gray-500 mb-4 hidden sm:block">
Drag &amp; drop files here, or click to select files.
</p>
<p class="text-gray-500 mb-4 sm:hidden">
Tap to select files or use the camera button below.
</p>
<input
id="fileInput"
@@ -26,19 +29,47 @@
onchange="handleFileSelect(event)"
name="files"
multiple
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
/>
<!-- Browse Files button (all devices) -->
<button
type="button"
onclick="document.getElementById('fileInput').click()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
style="min-height:44px;"
>
<i class="fas fa-folder-open mr-2"></i> Browse Files
</button>
<div class="text-sm text-gray-500 mt-2">
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
<p>Maximum size: 500MB per file</p>
</div>
</div>
<!-- Camera capture button - mobile only -->
<div class="mt-3 sm:hidden">
<label
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
style="min-height:44px;"
>
<i class="fas fa-camera"></i> Take Photo / Scan Document
<input
id="cameraInput"
type="file"
class="hidden"
onchange="handleFileSelect(event)"
accept="image/*"
capture="environment"
/>
</label>
</div>
</form>
</div>
<!-- URL Upload Section -->
<div class="w-full max-w-2xl mb-8">
<div class="w-full max-w-2xl mb-6 sm:mb-8">
<h2 class="text-xl font-semibold mb-4">Upload from URL</h2>
<div class="bg-white border border-gray-300 rounded-lg p-6">
<div class="bg-white border border-gray-300 rounded-lg p-4 sm:p-6">
<form id="urlUploadForm" class="space-y-4">
<div>
<label for="urlInput" class="block text-sm font-medium text-gray-700 mb-2">
@@ -49,7 +80,8 @@
id="urlInput"
name="url"
placeholder="https://example.com/document.pdf"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-height:44px;"
required
/>
<p class="text-sm text-gray-500 mt-1">
@@ -65,7 +97,8 @@
id="urlFilename"
name="filename"
placeholder="my-document.pdf"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-height:44px;"
/>
<p class="text-sm text-gray-500 mt-1">
Leave empty to use filename from URL
@@ -74,6 +107,7 @@
<button
type="submit"
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"
style="min-height:44px;"
>
Download and Process
</button>