mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-03 10:50:10 -05:00
3c3faf13d4
Migrate frontend from custom CSS to Tailwind CSS framework with comprehensive template updates and improved component structure. Breaking Changes: - Remove legacy CSS files (base.css, calendar.css, ui.css, etc.) - Replace with Tailwind-based styling system New Features: - Add Tailwind CSS configuration with PostCSS pipeline - Create new template components for admin, clients, invoices, projects, reports - Add form-bridge.css for smooth transition between legacy and Tailwind styles - Add default avatar SVG asset - Implement Tailwind-based kanban board template - Add comprehensive UI quick wins documentation Infrastructure: - Add package.json with Tailwind dependencies - Configure PostCSS and Tailwind build pipeline - Update .gitignore for Node modules and build artifacts Testing: - Add template rendering tests (test_tasks_templates.py) - Add UI component tests (test_ui_quick_wins.py) Templates Added: - Admin: dashboard, settings, system info, user management - Clients: list and detail views - Invoices: full CRUD templates with payment recording - Projects: list, detail, and Tailwind kanban views - Reports: comprehensive reporting templates - Timer: manual entry interface This commit represents the first phase of the UI redesign initiative, maintaining backward compatibility where needed while establishing the foundation for modern, responsive interfaces.
504 lines
35 KiB
HTML
504 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{ current_locale or 'en' }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='dist/output.css') }}">
|
|
<!-- Font Awesome -->
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='form-bridge.css') }}">
|
|
<style>
|
|
/* Minimal styles to properly align enhanced search UI */
|
|
.search-enhanced .search-input-wrapper { position: relative; }
|
|
.search-enhanced .search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: #A0AEC0; pointer-events: none; }
|
|
.dark .search-enhanced .search-icon { color: #718096; }
|
|
.search-enhanced .search-actions { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 0.25rem; }
|
|
.search-enhanced .search-actions .search-kbd { font-size: 11px; line-height: 1; padding: 2px 6px; border: 1px solid #E2E8F0; border-radius: 4px; color: #A0AEC0; }
|
|
.dark .search-enhanced .search-actions .search-kbd { border-color: #4A5568; color: #718096; }
|
|
/* Autocomplete dropdown: overlay below input */
|
|
.search-enhanced { position: relative; }
|
|
.search-enhanced .search-autocomplete { position: absolute; top: calc(100% + 4px); left: 0; right: 0; display: none; z-index: 50; background: #FFFFFF; color: #2D3748; border: 1px solid #E2E8F0; border-radius: 0.5rem; box-shadow: 0 8px 24px rgba(0,0,0,0.12); max-height: 22rem; overflow-y: auto; }
|
|
.dark .search-enhanced .search-autocomplete { background: #2D3748; color: #E2E8F0; border-color: #4A5568; }
|
|
.search-enhanced .search-autocomplete.show { display: block; }
|
|
.search-enhanced .search-section { padding: 0.5rem 0.5rem; }
|
|
.search-enhanced .search-section-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .06em; margin: 0.25rem 0.5rem; color: #A0AEC0; }
|
|
.dark .search-enhanced .search-section-title { color: #718096; }
|
|
.search-enhanced .search-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; text-decoration: none; color: inherit; }
|
|
.search-enhanced .search-item:hover, .search-enhanced .search-item.keyboard-focus { background: #F7F9FB; }
|
|
.dark .search-enhanced .search-item:hover, .dark .search-enhanced .search-item.keyboard-focus { background: #1A202C; }
|
|
.search-enhanced .search-item-title { font-weight: 500; }
|
|
/* Sidebar collapsed helpers */
|
|
.sidebar-collapsed .sidebar-label { display: none; }
|
|
.sidebar-collapsed .sidebar-header-title { display: none; }
|
|
.sidebar-collapsed #workDropdown, .sidebar-collapsed #insightsDropdown { display: none; }
|
|
.sidebar-collapsed #sidebar { width: 4rem !important; overflow-x: hidden; }
|
|
</style>
|
|
<script>
|
|
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
|
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark')
|
|
}
|
|
</script>
|
|
{% block extra_css %}{% endblock %}
|
|
</head>
|
|
<body class="bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark">
|
|
<script>
|
|
// Apply collapsed sidebar state ASAP to prevent flicker on navigations
|
|
(function(){
|
|
try { if (localStorage.getItem('sidebar-collapsed') === 'true') { document.documentElement.classList.add('sidebar-collapsed'); } } catch(_) {}
|
|
})();
|
|
</script>
|
|
<a href="#mainContentAnchor" class="sr-only focus:not-sr-only focus-ring absolute left-2 top-2 z-[1000] px-3 py-2 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded">Skip to content</a>
|
|
<div id="appShell" class="flex h-screen">
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="w-64 bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark p-4 flex-col hidden lg:flex transition-all duration-200 ease-in-out relative">
|
|
<div class="flex items-center mb-8">
|
|
<img src="{{ url_for('static', filename='images/drytrix-logo.svg') }}" alt="Logo" class="h-10 w-10 mr-2">
|
|
<h1 class="text-2xl font-bold text-primary sidebar-header-title"><a href="{{ url_for('main.dashboard') }}" class="no-underline">TimeTracker</a></h1>
|
|
</div>
|
|
<nav class="flex-1">
|
|
{% set ep = request.endpoint or '' %}
|
|
{% set work_open = ep.startswith('projects.') or ep.startswith('clients.') or ep.startswith('tasks.') or ep.startswith('timer.') or ep.startswith('kanban.') %}
|
|
{% set insights_open = ep.startswith('reports.') or ep.startswith('invoices.') or ep.startswith('analytics.') %}
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xs font-semibold text-text-muted-light dark:text-text-muted-dark uppercase tracking-wider sidebar-label">{{ _('Navigation') }}</h2>
|
|
<button id="sidebarCollapseBtn" class="p-1.5 rounded hover:bg-background-light dark:hover:bg-background-dark" aria-label="Toggle sidebar" title="Toggle sidebar">
|
|
<i id="sidebarCollapseIcon" class="fas fa-angles-left"></i>
|
|
</button>
|
|
</div>
|
|
<ul>
|
|
<li>
|
|
<a href="{{ url_for('main.dashboard') }}" class="flex items-center p-2 rounded-lg {% if ep == 'main.dashboard' %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
|
<i class="fas fa-tachometer-alt w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Dashboard') }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="mt-2">
|
|
<button onclick="toggleDropdown('workDropdown')" data-dropdown="workDropdown" class="w-full flex items-center p-2 rounded-lg {% if work_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
|
<i class="fas fa-briefcase w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Work') }}</span>
|
|
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
|
|
</button>
|
|
<ul id="workDropdown" class="{% if not work_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
|
|
{% set nav_active_projects = ep.startswith('projects.') %}
|
|
{% set nav_active_clients = ep.startswith('clients.') %}
|
|
{% set nav_active_tasks = ep.startswith('tasks.') %}
|
|
{% set nav_active_kanban = ep.startswith('kanban.') %}
|
|
{% set nav_active_timer = ep.startswith('timer.') %}
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_projects %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('projects.list_projects') }}">{{ _('Projects') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_clients %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('clients.list_clients') }}">{{ _('Clients') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_tasks %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('tasks.list_tasks') }}">{{ _('Tasks') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_kanban %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('kanban.board') }}">{{ _('Kanban') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_timer %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('timer.manual_entry') }}">{{ _('Log Time') }}</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="mt-2">
|
|
<button onclick="toggleDropdown('insightsDropdown')" data-dropdown="insightsDropdown" class="w-full flex items-center p-2 rounded-lg {% if insights_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
|
<i class="fas fa-chart-line w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Insights') }}</span>
|
|
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
|
|
</button>
|
|
<ul id="insightsDropdown" class="{% if not insights_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
|
|
{% set nav_active_reports = ep.startswith('reports.') %}
|
|
{% set nav_active_invoices = ep.startswith('invoices.') %}
|
|
{% set nav_active_analytics = ep.startswith('analytics.') %}
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_reports %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('reports.reports') }}">{{ _('Reports') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_invoices %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('invoices.list_invoices') }}">{{ _('Invoices') }}</a>
|
|
</li>
|
|
<li>
|
|
<a class="block px-2 py-1 rounded {% if nav_active_analytics %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('analytics.analytics_dashboard') }}">{{ _('Analytics') }}</a>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
{% if current_user.is_admin %}
|
|
<li class="mt-2">
|
|
<a href="{{ url_for('admin.admin_dashboard') }}" class="flex items-center p-2 rounded-lg {% if ep == 'admin.admin_dashboard' %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
|
<i class="fas fa-cog w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Admin') }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="mt-2">
|
|
<a href="{{ url_for('admin.oidc_debug') }}" class="flex items-center p-2 rounded-lg {% if ep == 'admin.oidc_debug' %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
|
|
<i class="fas fa-shield-alt w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">OIDC</span>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
<div class="mt-auto pt-4 border-t border-border-light dark:border-border-dark">
|
|
<ul>
|
|
<li>
|
|
<a href="{{ url_for('main.about') }}" class="flex items-center p-2 rounded-lg text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-circle-info w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('About') }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="mt-2">
|
|
<a href="{{ url_for('main.help') }}" class="flex items-center p-2 rounded-lg text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-life-ring w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Help') }}</span>
|
|
</a>
|
|
</li>
|
|
<li class="mt-2">
|
|
<a href="https://buymeacoffee.com/DryTrix" target="_blank" rel="noopener noreferrer" class="flex items-center p-2 rounded-lg text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-mug-saucer w-6 text-center"></i>
|
|
<span class="ml-3 sidebar-label">{{ _('Buy me a coffee') }}</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<div id="mainContent" class="flex-1 flex flex-col transition-all duration-200 ease-in-out">
|
|
<!-- Header -->
|
|
<header class="bg-card-light dark:bg-card-dark p-4 border-b border-border-light dark:border-border-dark flex justify-between items-center">
|
|
<!-- Mobile menu button -->
|
|
<button id="mobileSidebarBtn" class="lg:hidden" aria-label="Open sidebar">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
|
|
<!-- Search + Command button -->
|
|
<div class="hidden sm:flex items-center gap-2">
|
|
<div class="w-96">
|
|
<form class="navbar-search" role="search" action="{{ url_for('main.search') }}" method="get" autocomplete="off">
|
|
<label for="search" class="sr-only">{{ _('Search') }}</label>
|
|
<input id="search" name="q" type="search" placeholder="{{ _('Search') }}" aria-label="{{ _('Search') }}" data-enhanced-search='{"endpoint": "/api/search", "minChars": 2, "maxResults": 10}' class="w-full bg-background-light dark:bg-gray-700 border border-transparent dark:border-transparent rounded-lg py-2 pl-10 pr-14 text-text-light dark:text-text-dark placeholder-text-muted-light dark:placeholder-text-muted-dark focus:outline-none focus:ring-2 focus:ring-primary" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
|
|
</form>
|
|
</div>
|
|
<button type="button" class="text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-label="Open command palette" onclick="window.openCommandPalette && window.openCommandPalette();">
|
|
<i class="fa-solid fa-terminal"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Right side controls -->
|
|
<div class="flex items-center space-x-4">
|
|
<button id="theme-toggle" type="button" class="text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-label="{{ _('Toggle dark mode') }}">
|
|
<i id="theme-toggle-dark-icon" class="hidden fa-solid fa-moon w-5 h-5"></i>
|
|
<i id="theme-toggle-light-icon" class="hidden fa-solid fa-sun w-5 h-5"></i>
|
|
</button>
|
|
|
|
<!-- Language Switcher -->
|
|
<div class="relative z-50">
|
|
<button onclick="toggleDropdown('langDropdown')" class="flex items-center text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-haspopup="true" aria-expanded="false" aria-controls="langDropdown">
|
|
<i class="fas fa-globe"></i>
|
|
</button>
|
|
<ul id="langDropdown" class="hidden absolute right-0 mt-2 w-48 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg">
|
|
<li class="dropdown-header p-2 text-sm text-text-muted-light dark:text-text-muted-dark">{{ _('Language') }}</li>
|
|
{% for code, label in config['LANGUAGES'].items() %}
|
|
<li>
|
|
<a class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700" href="{{ url_for('main.set_language') }}?lang={{ code }}">
|
|
{{ label }}
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- User Profile -->
|
|
<div class="relative">
|
|
<button onclick="toggleDropdown('userDropdown')" class="flex items-center" aria-label="{{ _('User menu') }}" aria-haspopup="true" aria-expanded="false" aria-controls="userDropdown">
|
|
<img src="{{ url_for('static', filename='images/avatar-default.svg') }}" alt="{{ current_user.display_name }}" class="w-8 h-8 rounded-full">
|
|
<span class="ml-2 hidden md:inline">{{ current_user.display_name }}</span>
|
|
<i class="fas fa-chevron-down ml-1 hidden md:inline" aria-hidden="true"></i>
|
|
</button>
|
|
<ul id="userDropdown" class="hidden absolute right-0 mt-2 w-48 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg">
|
|
<li><a href="{{ url_for('auth.profile') }}" class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700">{{ _('Profile') }}</a></li>
|
|
<li><a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700">{{ _('Logout') }}</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Flash Messages (hidden, converted to toasts by toast-notifications.js) -->
|
|
<div id="flash-messages-container" class="hidden">
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert {% if category == 'success' %}alert-success{% elif category == 'error' %}alert-danger{% elif category == 'warning' %}alert-warning{% else %}alert-info{% endif %}" data-toast-message="{{ message }}" data-toast-type="{{ category }}"></div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
</div>
|
|
|
|
<!-- Main page content -->
|
|
<main id="mainContentAnchor" class="flex-1 p-6 overflow-y-auto">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Mobile Bottom Navigation -->
|
|
<nav class="lg:hidden fixed bottom-0 inset-x-0 bg-card-light dark:bg-card-dark border-t border-border-light dark:border-border-dark flex justify-around">
|
|
<a href="{{ url_for('main.dashboard') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-tachometer-alt"></i>
|
|
<span class="text-xs">{{ _('Dashboard') }}</span>
|
|
</a>
|
|
<a href="{{ url_for('projects.list_projects') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-briefcase"></i>
|
|
<span class="text-xs">{{ _('Projects') }}</span>
|
|
</a>
|
|
<a href="{{ url_for('timer.manual_entry') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-plus-circle text-2xl"></i>
|
|
</a>
|
|
<a href="{{ url_for('reports.reports') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-chart-line"></i>
|
|
<span class="text-xs">{{ _('Reports') }}</span>
|
|
</a>
|
|
<a href="{{ url_for('auth.profile') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-user-circle"></i>
|
|
<span class="text-xs">{{ _('Profile') }}</span>
|
|
</a>
|
|
</nav>
|
|
|
|
</div>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='toast-notifications.css') }}">
|
|
<!-- Enhanced UI scripts -->
|
|
<script src="{{ url_for('static', filename='enhanced-search.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='toast-notifications.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='enhanced-tables.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='interactions.js') }}"></script>
|
|
<!-- Old command palette and keyboard navigation (restored) -->
|
|
<script src="{{ url_for('static', filename='commands.js') }}"></script>
|
|
<script>
|
|
// Minimal global shortcuts: Ctrl+K (focus search), Ctrl+Shift+L (toggle theme), 't' (toggle timer)
|
|
(function(){
|
|
function isTyping(e){
|
|
const t = e.target; const tag = (t && t.tagName || '').toLowerCase();
|
|
return tag === 'input' || tag === 'textarea' || tag === 'select' || (t && t.isContentEditable);
|
|
}
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl/Cmd + K -> focus search
|
|
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && (e.key === 'k' || e.key === 'K')) {
|
|
e.preventDefault();
|
|
const search = document.getElementById('search');
|
|
if (search) { search.focus(); search.select && search.select(); }
|
|
}
|
|
});
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl/Cmd + Shift + L -> toggle theme
|
|
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'l' || e.key === 'L')) {
|
|
e.preventDefault();
|
|
document.getElementById('theme-toggle')?.click();
|
|
}
|
|
});
|
|
async function toggleTimer(){
|
|
try {
|
|
const statusRes = await fetch("{{ url_for('timer.timer_status') }}", { credentials: 'same-origin' });
|
|
const status = await statusRes.json();
|
|
if (status && status.active) {
|
|
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
const stopRes = await fetch("{{ url_for('timer.stop_timer') }}", { method: 'POST', headers: { 'X-CSRF-Token': token }, credentials: 'same-origin' });
|
|
if (stopRes.ok) { window.toastManager && window.toastManager.info('Timer stopped'); }
|
|
window.location.href = "{{ url_for('main.dashboard') }}";
|
|
} else {
|
|
window.location.href = "{{ url_for('timer.manual_entry') }}";
|
|
}
|
|
} catch (_) { window.location.href = "{{ url_for('timer.manual_entry') }}"; }
|
|
}
|
|
document.addEventListener('keydown', (e) => {
|
|
if (isTyping(e)) return;
|
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 't' || e.key === 'T')) {
|
|
e.preventDefault();
|
|
toggleTimer();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
<script>
|
|
// Sidebar collapse logic with persisted state
|
|
(function(){
|
|
const appShell = document.getElementById('appShell');
|
|
const sidebar = document.getElementById('sidebar');
|
|
const main = document.getElementById('mainContent');
|
|
const collapseBtn = document.getElementById('sidebarCollapseBtn');
|
|
const mobileBtn = document.getElementById('mobileSidebarBtn');
|
|
let flyout;
|
|
|
|
function applyCollapsed(isCollapsed){
|
|
// Toggle a class on the shell to let Tailwind width utilities react
|
|
const icon = document.getElementById('sidebarCollapseIcon');
|
|
if (isCollapsed){
|
|
appShell.classList.add('sidebar-collapsed');
|
|
sidebar.classList.add('w-16');
|
|
sidebar.classList.remove('w-64');
|
|
icon && icon.classList.remove('fa-angles-left');
|
|
icon && icon.classList.add('fa-angles-right');
|
|
} else {
|
|
appShell.classList.remove('sidebar-collapsed');
|
|
sidebar.classList.remove('w-16');
|
|
sidebar.classList.add('w-64');
|
|
icon && icon.classList.remove('fa-angles-right');
|
|
icon && icon.classList.add('fa-angles-left');
|
|
}
|
|
}
|
|
|
|
// Initialize from storage
|
|
try {
|
|
const saved = localStorage.getItem('sidebar-collapsed');
|
|
applyCollapsed(saved === 'true');
|
|
} catch(_) {}
|
|
|
|
// Desktop toggle
|
|
collapseBtn && collapseBtn.addEventListener('click', function(){
|
|
const nowCollapsed = !appShell.classList.contains('sidebar-collapsed');
|
|
applyCollapsed(nowCollapsed);
|
|
try { localStorage.setItem('sidebar-collapsed', String(nowCollapsed)); } catch(_) {}
|
|
});
|
|
|
|
// Mobile toggle simply shows/hides sidebar via utility classes
|
|
mobileBtn && mobileBtn.addEventListener('click', function(){
|
|
// On small screens we can temporarily show sidebar as overlay
|
|
const showing = sidebar.classList.contains('hidden');
|
|
sidebar.classList.toggle('hidden', !showing);
|
|
sidebar.classList.toggle('fixed', showing);
|
|
sidebar.classList.toggle('inset-y-0', showing);
|
|
sidebar.classList.toggle('z-50', showing);
|
|
// Position the toggle icon correctly when overlayed
|
|
const iconBtn = document.getElementById('sidebarCollapseBtn');
|
|
if (iconBtn){ iconBtn.classList.toggle('right-3', showing); iconBtn.classList.toggle('-right-3', !showing); }
|
|
});
|
|
|
|
// Flyout submenu when collapsed
|
|
function hideFlyout(){
|
|
if (flyout){ flyout.classList.add('hidden'); flyout.innerHTML=''; }
|
|
}
|
|
function ensureFlyout(){
|
|
if (!flyout){
|
|
flyout = document.createElement('div');
|
|
flyout.id = 'sidebarFlyout';
|
|
flyout.className = 'hidden fixed z-50 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg p-2';
|
|
document.body.appendChild(flyout);
|
|
document.addEventListener('click', (e) => {
|
|
if (!flyout || flyout.classList.contains('hidden')) return;
|
|
const inside = flyout.contains(e.target);
|
|
const trigger = e.target.closest('[data-dropdown]');
|
|
if (!inside && !trigger) hideFlyout();
|
|
});
|
|
window.addEventListener('resize', hideFlyout);
|
|
window.addEventListener('scroll', hideFlyout, true);
|
|
}
|
|
}
|
|
function showFlyout(triggerBtn, listId){
|
|
ensureFlyout();
|
|
const list = document.getElementById(listId);
|
|
if (!list) return;
|
|
flyout.innerHTML = '';
|
|
const ul = document.createElement('ul');
|
|
ul.className = 'min-w-[12rem] space-y-1';
|
|
list.querySelectorAll('a').forEach((a) => {
|
|
const li = document.createElement('li');
|
|
const link = a.cloneNode(true);
|
|
link.classList.add('block','px-3','py-2','rounded','hover:bg-background-light','dark:hover:bg-background-dark');
|
|
li.appendChild(link);
|
|
ul.appendChild(li);
|
|
});
|
|
flyout.appendChild(ul);
|
|
const rect = triggerBtn.getBoundingClientRect();
|
|
const sbRect = sidebar.getBoundingClientRect();
|
|
const left = sbRect.right + 8 + window.scrollX;
|
|
const top = rect.top + window.scrollY;
|
|
flyout.style.left = left + 'px';
|
|
flyout.style.top = top + 'px';
|
|
flyout.classList.remove('hidden');
|
|
}
|
|
sidebar.addEventListener('click', function(e){
|
|
const btn = e.target.closest('[data-dropdown]');
|
|
if (!btn) return;
|
|
const listId = btn.getAttribute('data-dropdown');
|
|
const isCollapsed = appShell.classList.contains('sidebar-collapsed') || document.documentElement.classList.contains('sidebar-collapsed');
|
|
if (isCollapsed){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
showFlyout(btn, listId);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
<!-- Command Palette Modal (restored, Tailwind-styled, no Bootstrap required) -->
|
|
<div id="commandPaletteModal" class="fixed inset-0 z-50 hidden">
|
|
<div class="absolute inset-0 bg-black/50" onclick="document.getElementById('commandPaletteModal').classList.add('hidden')"></div>
|
|
<div class="relative max-w-2xl mx-auto mt-24 bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark rounded-lg shadow-xl">
|
|
<div class="p-3 sm:p-4 border-b border-border-light dark:border-border-dark flex items-center justify-between">
|
|
<div id="commandPaletteHelp" class="text-xs text-text-muted-light dark:text-text-muted-dark"></div>
|
|
<button id="commandPaletteClose" class="px-2 py-1 text-sm hover:bg-background-light dark:hover:bg-background-dark rounded">Close</button>
|
|
</div>
|
|
<div class="p-3 sm:p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<i class="fas fa-terminal text-text-muted-light dark:text-text-muted-dark"></i>
|
|
<input id="commandPaletteInput" type="text" class="w-full bg-background-light dark:bg-gray-700 border border-border-light dark:border-border-dark rounded-lg py-2 px-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Type a command or search..." autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
|
|
</div>
|
|
<div id="commandPaletteList" class="flex flex-col max-h-96 overflow-y-auto divide-y divide-border-light dark:divide-border-dark"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
|
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
|
|
|
// Change the icons inside the button based on previous settings
|
|
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
themeToggleLightIcon.classList.remove('hidden');
|
|
} else {
|
|
themeToggleDarkIcon.classList.remove('hidden');
|
|
}
|
|
|
|
var themeToggleBtn = document.getElementById('theme-toggle');
|
|
|
|
themeToggleBtn.addEventListener('click', function() {
|
|
|
|
// toggle icons inside button
|
|
themeToggleDarkIcon.classList.toggle('hidden');
|
|
themeToggleLightIcon.classList.toggle('hidden');
|
|
|
|
// if set via local storage previously
|
|
if (localStorage.getItem('color-theme')) {
|
|
if (localStorage.getItem('color-theme') === 'light') {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.setItem('color-theme', 'dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.setItem('color-theme', 'light');
|
|
}
|
|
|
|
// if NOT set via local storage previously
|
|
} else {
|
|
if (document.documentElement.classList.contains('dark')) {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.setItem('color-theme', 'light');
|
|
} else {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.setItem('color-theme', 'dark');
|
|
}
|
|
}
|
|
});
|
|
|
|
function toggleDropdown(id) {
|
|
const dropdown = document.getElementById(id);
|
|
dropdown.classList.toggle('hidden');
|
|
}
|
|
</script>
|
|
{% block scripts_extra %}{% endblock %}
|
|
</body>
|
|
</html>
|
|
|