'use strict';
const {
escapeHtmlAttribute: drawerEscapeHtmlAttribute,
escapeHtml: drawerEscapeHtml,
} = globalThis.TabOutIconUtils || {};
const {
t: drawerT,
} = globalThis.TabHarborI18n || {};
const {
clampTriggerTop: drawerClampTriggerTop,
normalizeTriggerPosition: drawerNormalizeTriggerPosition,
} = globalThis.TabOutDeferredTriggerPosition || {};
const {
clearArchivedTodos: drawerClearArchivedTodos,
completeTodo: drawerCompleteTodo,
createTodo: drawerCreateTodo,
deleteTodo: drawerDeleteTodo,
normalizeTodos: drawerNormalizeTodos,
searchTodos: drawerSearchTodos,
splitTodos: drawerSplitTodos,
updateTodo: drawerUpdateTodo,
} = globalThis.TabOutTodosStore || {};
let deferredPanelOpen = false;
let deferredTriggerPosition = drawerNormalizeTriggerPosition ? drawerNormalizeTriggerPosition() : { top: null };
const DEFERRED_TRIGGER_POSITION_KEY = 'deferredTriggerPosition';
let deferredTriggerDragState = null;
let deferredTriggerSuppressClickUntil = 0;
let drawerView = 'todos';
let todoDetailId = '';
let todoSearchOpen = false;
let todoSearchQuery = '';
let drawerFocusReturnEl = null;
const TODOS_KEY = 'todos';
function createTodoEditorState(input = {}) {
return {
open: Boolean(input.open),
mode: input.mode === 'edit' ? 'edit' : 'create',
todoId: input.todoId || '',
title: input.title || '',
description: input.description || '',
error: input.error || '',
};
}
let todoEditorState = createTodoEditorState();
function drawerTodoText(key, fallback) {
return drawerT ? drawerT(key) : fallback;
}
function drawerEscapeAttr(value = '') {
if (drawerEscapeHtmlAttribute) return drawerEscapeHtmlAttribute(value);
return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/
${drawerEscapeHtml ? drawerEscapeHtml(todo.title) : todo.title}
${ago}
`;
}
function renderTodoListItem(todo, { dragEnabled = true } = {}) {
const ago = timeAgo(todo.createdAt);
const dragHandle = dragEnabled
? `
`
: '';
return `
`;
}
function renderTodoDetail(todo) {
return `
Back to list
${drawerEscapeHtml ? drawerEscapeHtml(todo.title) : todo.title}
${drawerEscapeHtml ? drawerEscapeHtml(todo.description || 'Add a note when this task needs more context.') : (todo.description || 'Add a note when this task needs more context.')}
Created ${timeAgo(todo.createdAt)}
`;
}
function renderTodoEditor() {
const isEdit = todoEditorState.mode === 'edit';
const editorTitle = isEdit
? drawerTodoText('todoEditorEditTitle', 'Edit todo')
: drawerTodoText('todoEditorCreateTitle', 'New todo');
const titleLabel = drawerTodoText('todoTitleLabel', 'Title');
const descriptionLabel = drawerTodoText('todoDescriptionLabel', 'Details');
const titlePlaceholder = drawerTodoText('todoEditorTitlePlaceholder', 'What needs attention?');
const descriptionPlaceholder = drawerTodoText('todoEditorDescriptionPlaceholder', 'Add notes, context, or a next step.');
const titleValue = drawerEscapeAttr(todoEditorState.title);
const descriptionValue = drawerEscapeHtml ? drawerEscapeHtml(todoEditorState.description) : todoEditorState.description;
const errorHtml = todoEditorState.error
? `${drawerEscapeHtml ? drawerEscapeHtml(todoEditorState.error) : todoEditorState.error}
`
: '
';
return `
`;
}
async function renderTodoPanel() {
const panel = document.getElementById('todoPanel');
const countEl = document.getElementById('todoCount');
const list = document.getElementById('todoList');
const empty = document.getElementById('todoEmpty');
const archive = document.getElementById('todoArchive');
const archiveCount = document.getElementById('todoArchiveCount');
const archiveList = document.getElementById('todoArchiveList');
const clearArchiveBtn = document.getElementById('clearTodoArchiveBtn');
const detail = document.getElementById('todoDetailView');
const searchWrap = document.getElementById('todoSearchWrap');
const searchInput = document.getElementById('todoSearchInput');
const todoSearchToggle = document.getElementById('todoSearchToggle');
const todoArchiveToggle = document.getElementById('todoArchiveToggle');
const todoArchiveBody = document.getElementById('todoArchiveBody');
if (!panel) return;
const todos = await getTodos();
const { active, archived } = drawerSplitTodos(todos);
const filtered = drawerSearchTodos(active, todoSearchQuery);
const todoDragEnabled = !todoSearchQuery.trim() && !todoDetailId && !todoEditorState.open;
let editor = document.getElementById('todoEditorView');
countEl.textContent = `${active.length}`;
if (todoSearchToggle) {
todoSearchToggle.setAttribute('aria-expanded', String(todoSearchOpen));
}
searchWrap.style.display = todoSearchOpen ? 'block' : 'none';
searchWrap.hidden = !todoSearchOpen;
if (searchInput && searchInput.value !== todoSearchQuery) searchInput.value = todoSearchQuery;
if (todoArchiveToggle && todoArchiveBody) {
const archiveExpanded = todoArchiveBody.style.display !== 'none' && !todoArchiveBody.hidden;
todoArchiveToggle.setAttribute('aria-expanded', String(archiveExpanded));
}
if (todoEditorState.open) {
if (editor) {
const wrapper = document.createElement('div');
wrapper.innerHTML = renderTodoEditor().trim();
editor.replaceWith(wrapper.firstElementChild);
} else {
detail.insertAdjacentHTML('beforebegin', renderTodoEditor());
}
editor = document.getElementById('todoEditorView');
if (editor) editor.style.display = 'block';
detail.style.display = 'none';
list.style.display = 'none';
empty.style.display = 'none';
} else if (editor) {
editor.style.display = 'none';
editor.innerHTML = '';
}
if (!todoEditorState.open && todoDetailId) {
const todo = active.find(item => item.id === todoDetailId);
if (todo) {
detail.innerHTML = renderTodoDetail(todo);
detail.style.display = 'block';
list.style.display = 'none';
empty.style.display = 'none';
} else {
todoDetailId = '';
detail.style.display = 'none';
}
} else {
detail.style.display = 'none';
}
if (!todoEditorState.open && !todoDetailId) {
if (filtered.length > 0) {
list.innerHTML = filtered.map(todo => renderTodoListItem(todo, { dragEnabled: todoDragEnabled })).join('');
list.style.display = 'block';
empty.style.display = 'none';
} else {
list.style.display = 'none';
empty.style.display = 'block';
}
}
if (todoEditorState.open) {
archive.style.display = 'none';
if (clearArchiveBtn) clearArchiveBtn.style.display = 'none';
} else if (archived.length > 0) {
archiveCount.textContent = `(${archived.length})`;
archiveList.innerHTML = archived.map(todo => renderTodoArchiveItem(todo)).join('');
archive.style.display = 'block';
if (clearArchiveBtn) clearArchiveBtn.style.display = 'inline-flex';
} else {
archive.style.display = 'none';
if (clearArchiveBtn) clearArchiveBtn.style.display = 'none';
}
}
async function renderDeferredColumn() {
const column = document.getElementById('drawerColumn');
const triggerStack = document.getElementById('drawerTriggerStack');
const todoTrigger = document.getElementById('todoTrigger');
const overlay = document.getElementById('deferredOverlay');
const todoPanel = document.getElementById('todoPanel');
const titleButtons = document.querySelectorAll('.drawer-title-btn');
if (!column) return;
try {
await getTodos();
await loadDeferredTriggerPosition();
column.style.display = 'block';
if (triggerStack) triggerStack.style.display = 'flex';
if (todoTrigger) todoTrigger.style.display = 'inline-flex';
column.classList.toggle('open', deferredPanelOpen);
column.setAttribute('aria-hidden', String(!deferredPanelOpen));
if (todoTrigger) {
todoTrigger.setAttribute('aria-expanded', String(deferredPanelOpen));
todoTrigger.setAttribute('aria-label', deferredPanelOpen ? 'Close todos' : 'Open todos');
}
overlay.hidden = !deferredPanelOpen;
overlay.classList.toggle('visible', deferredPanelOpen);
applyDeferredTriggerPosition();
todoPanel?.classList.add('is-active');
titleButtons.forEach(button => {
button.classList.add('is-active');
button.setAttribute('aria-selected', 'true');
button.tabIndex = 0;
});
await renderTodoPanel();
} catch (err) {
console.warn('[tab-harbor] Could not load todos:', err);
column.style.display = 'none';
if (triggerStack) triggerStack.style.display = 'none';
overlay.hidden = true;
overlay.classList.remove('visible');
}
}
function setDeferredPanelOpen(nextOpen) {
const shouldOpen = Boolean(nextOpen);
if (shouldOpen) {
drawerFocusReturnEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
}
deferredPanelOpen = shouldOpen;
renderDeferredColumn().then(() => {
if (deferredPanelOpen) {
const preferredFocusId = todoSearchOpen ? 'todoSearchInput' : 'todoPanel';
const preferredTarget = document.getElementById(preferredFocusId);
if (preferredTarget?.focus) {
preferredTarget.focus({ preventScroll: true });
} else {
focusFirstElement(document.getElementById('drawerColumn'));
}
return;
}
drawerFocusReturnEl?.focus?.({ preventScroll: true });
drawerFocusReturnEl = null;
});
}