task-track 0.6.1

A JJ workspace-based task and TODO management CLI tool
Documentation
{% extends "base.html" %}

{% block title %}
{% if task %}Task #{{ task.id }}: {{ task.name }}{% else %}Track WebUI{% endif %}
{% endblock %}

{% block toolbar_buttons %}
{% if task %}
<!-- View mode toggle button -->
<button class="toolbar-btn" onclick="toggleViewMode()" id="mode-toggle-btn">
    <span id="mode-icon">👁️</span> <span id="mode-label">Overview</span>
</button>
{% endif %}
{{ super() }}
{% endblock %}

{% block content %}
{% if task %}
<!-- Task Header -->
<header class="task-header">
    <h1>{{ task.name }}{% if task.alias %} <span class="task-alias">({{ task.alias }})</span>{% endif %}</h1>
    <div class="task-meta">
        {% if task.ticket_id %}
        <span class="badge">{{ task.ticket_id }}</span>
        {% else %}
        <span class="task-id">Task #{{ task.id }}</span>
        {% endif %}
    </div>
</header>


<!-- Overview Section (hidden in Focus Mode) -->
<div id="overview-section">
    {% if task.is_today_task %}
    <!-- For today task: Show Calendar and Links in 2-pane layout -->
    <div class="two-pane-layout">
        <!-- Calendar Section (Left) -->
        {% include "partials/calendar.html" %}

        <!-- Links Section (Right) -->
        {% include "partials/links.html" %}
    </div>
    {% else %}
    <!-- For regular task: Show Description, Ticket, Repositories, and Links -->
    <!-- 2-Pane Layout for Description and Ticket -->
    <div class="two-pane-layout">
        <!-- Description Section (Left) -->
        {% include "partials/description.html" %}

        <!-- Ticket Section (Right) -->
        {% include "partials/ticket.html" %}
    </div>

    <!-- 2-Pane Layout for Repositories and Links -->
    <div class="two-pane-layout">
        <!-- Repositories Section (Left) -->
        {% include "partials/repos.html" %}

        <!-- Links Section (Right) -->
        {% include "partials/links.html" %}
    </div>
    {% endif %}
</div>


<!-- 2-Pane Layout for TODOs and Scraps -->
<div class="two-pane-layout main-content">
    <!-- TODOs Section (Left) -->
    {% include "partials/todo_list.html" %}

    <!-- Scraps Section (Right) -->
    {% include "partials/scrap_list.html" %}
</div>
{% else %}
<!-- No Task State -->
<div class="no-task">
    <div class="icon">📋</div>
    <h2>No Active Task</h2>
    <p>Create a new task or switch to an existing one to get started.</p>
    <p style="margin-top: 1rem;">
        Run <code>track new "Task name"</code> to create a new task.
    </p>
</div>
{% endif %}

<script>
    // Scroll to the oldest pending task in the TODO list and highlight it
    function scrollToOldestPendingTask() {
        // Remove highlight from all todo items first
        document.querySelectorAll('.todo-item').forEach(item => {
            item.classList.remove('oldest-pending');
        });

        // Find the first pending todo item
        const firstPendingTodo = document.querySelector('.todo-item.pending');

        if (firstPendingTodo) {
            // Add highlight class to the oldest pending task
            firstPendingTodo.classList.add('oldest-pending');

            // Get the list container
            const listContainer = document.querySelector('#todos-section .list-container');

            if (listContainer) {
                // Calculate the position of the todo item relative to the list container
                const todoRect = firstPendingTodo.getBoundingClientRect();
                const containerRect = listContainer.getBoundingClientRect();
                const scrollOffset = todoRect.top - containerRect.top + listContainer.scrollTop;

                // Scroll to the position with smooth behavior
                listContainer.scrollTo({
                    top: scrollOffset,
                    behavior: 'smooth'
                });
            }
        }
    }

    // Toggle between Focus Mode and Overview Mode
    function toggleViewMode() {
        const section = document.getElementById('overview-section');
        const icon = document.getElementById('mode-icon');
        const label = document.getElementById('mode-label');
        const body = document.body;

        if (section.style.display === 'none') {
            // Switch to Overview Mode
            section.style.display = 'block';
            icon.textContent = '👁️';
            label.textContent = 'Overview';
            body.classList.remove('focus-mode');
            localStorage.setItem('viewMode', 'overview');

            // Remove highlight when leaving focus mode
            document.querySelectorAll('.todo-item').forEach(item => {
                item.classList.remove('oldest-pending');
            });
        } else {
            // Switch to Focus Mode
            section.style.display = 'none';
            icon.textContent = '🎯';
            label.textContent = 'Focus';
            body.classList.add('focus-mode');
            localStorage.setItem('viewMode', 'focus');

            // Scroll to and highlight oldest pending task when entering focus mode
            setTimeout(scrollToOldestPendingTask, 100);
        }
    }

    // Restore view mode state from localStorage
    function restoreViewMode() {
        const viewMode = localStorage.getItem('viewMode') || 'overview';
        const section = document.getElementById('overview-section');
        const icon = document.getElementById('mode-icon');
        const label = document.getElementById('mode-label');
        const body = document.body;

        if (viewMode === 'focus') {
            if (section && icon && label) {
                section.style.display = 'none';
                icon.textContent = '🎯';
                label.textContent = 'Focus';
                body.classList.add('focus-mode');

                // Scroll to and highlight oldest pending task when restoring focus mode
                setTimeout(scrollToOldestPendingTask, 100);
            }
        } else {
            if (section && icon && label) {
                section.style.display = 'block';
                icon.textContent = '👁️';
                label.textContent = 'Overview';
                body.classList.remove('focus-mode');

                // Remove highlight in overview mode
                document.querySelectorAll('.todo-item').forEach(item => {
                    item.classList.remove('oldest-pending');
                });
            }
        }
    }

    // Initialize view mode on page load
    document.addEventListener('DOMContentLoaded', restoreViewMode);

    // Restore view mode after HTMX content updates
    document.body.addEventListener('htmx:afterSwap', restoreViewMode);
    document.body.addEventListener('htmx:afterSettle', restoreViewMode);

    // Scroll to scraps related to a specific todo
    function scrollToRelatedScraps(event, todoId) {
        event.stopPropagation(); // Prevent menu from opening

        // Find all scrap items
        const scrapItems = document.querySelectorAll('.scrap-item');

        if (scrapItems.length === 0) {
            console.log('No scraps found');
            return;
        }

        // Remove existing highlights
        scrapItems.forEach(item => item.classList.remove('scrap-highlighted'));

        // Find scraps with matching active_todo_id
        // We need to get this from the server-rendered data
        // For now, we'll use a data attribute approach
        let foundScraps = [];

        scrapItems.forEach(item => {
            // The active_todo_id should be added as a data attribute in the template
            const activeTodoId = item.getAttribute('data-active-todo-id');
            if (activeTodoId && parseInt(activeTodoId) === todoId) {
                foundScraps.push(item);
            }
        });

        if (foundScraps.length === 0) {
            console.log(`No scraps found for todo #${todoId}`);
            // Show a subtle notification
            const statusText = document.querySelector('#connection-status .status-text');
            if (statusText) {
                const originalText = statusText.textContent;
                statusText.textContent = `No scraps for todo #${todoId}`;
                setTimeout(() => {
                    statusText.textContent = originalText;
                }, 2000);
            }
            return;
        }

        // Scroll to the first matching scrap
        const targetScrap = foundScraps[0];
        targetScrap.classList.add('scrap-highlighted');

        const listContainer = document.querySelector('#scraps-section .list-container');
        if (listContainer) {
            const scrapRect = targetScrap.getBoundingClientRect();
            const containerRect = listContainer.getBoundingClientRect();
            const scrollOffset = scrapRect.top - containerRect.top + listContainer.scrollTop;

            listContainer.scrollTo({
                top: scrollOffset - 20, // Add some padding
                behavior: 'smooth'
            });
        }

        // Remove highlight after animation completes
        setTimeout(() => {
            targetScrap.classList.remove('scrap-highlighted');
        }, 3500);

        console.log(`Found ${foundScraps.length} scrap(s) for todo #${todoId}`);
    }


</script>
{% endblock %}