break out css and js

Hunter
Sunday, March 8th, 2026 at 9:49 AM

index.html+16-1981
readme.md+1-1
resources/keyboard.jsnew
resources/main.cssnew
resources/main.jsnew
resources/multi-select.jsnew
resources/navigation.jsnew
resources/render.jsnew
resources/reorganize.jsnew
resources/state.jsnew
resources/storage.jsnew
resources/task-element.jsnew
resources/task-model.jsnew
resources/theme.jsnew
sw.jsnew

index.html
10  	<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🪆</text></svg>">
11  
12  	<title>todo</title>
   -	<style>
   -		/* custom colors */
   -		:root {
   -			--coconut: #ffffff;
   -			--licorice: #000000;
   -			--wasabi: #ccff00;
   -			--eggplant: #7700ff;
   -
   -			--murk: #8e9918;
   -			--tadpole: #27350f;
   -			--lilypad: #6f8823;
   -			--reed: #4a5d23;
   -
   -			--pollen: #f4a127;
   -			--loam: #5a352b;
   -			--chlorophyll: #5aa83b;
   -			--terracotta: #b15c2e;
   -
   -			--wheat: #d2c3a3;
   -			--earth: #4a3c31;
   -			--pumpkin: #cb7c52;
   -			--tobacco: #7d6c55;
   -
   -			--moss: #20302f;
   -			--goat-milk: #d8d3c9;
   -			--burl: #231f20;
   -			--flame: #c63728;
   -
   -			--soot: #181a1b;
   -			--smoke: #e8e6e3;
   -			--ash: #252829;
   -			--cinder: #ff5532;
   -
   -			--blue-raspberry: #0458b3;
   -			--cola: #1e1515;
   -			--bubblegum: #e11761;
   -			--taffy: #eaeaea;
   -
   -			--muscat: #663399;
   -			--cask: #722f37;
   -			--bloom: #f8f6ff;
   -		}
   -
   -		/* custom fonts */
   -		@font-face {
   -			font-family: BasteleurMoonlight;
   -			src: url('resources/fonts/Basteleur-Moonlight.ttf') format('truetype');
   -		}
   -
   -		/* themes */
   -		:root[data-theme="gak"] {
   -			--background: var(--coconut);
   -			--text: var(--licorice);
   -			--highlight: var(--wasabi);
   -			--accent: var(--eggplant);
   -			--icon: '🫟';
   -		}
   -
   -		:root[data-theme="swamp"] {
   -			--background: var(--murk);
   -			--text: var(--tadpole);
   -			--highlight: var(--lilypad);
   -			--accent: var(--reed);
   -			--icon: '🦠';
   -		}
   -
   -		:root[data-theme="sunflower"] {
   -			--background: var(--pollen);
   -			--text: var(--loam);
   -			--highlight: var(--chlorophyll);
   -			--accent: var(--terracotta);
   -			--icon: '🌻';
   -		}
   -
   -		:root[data-theme="harvest"] {
   -			--background: var(--wheat);
   -			--text: var(--earth);
   -			--highlight: var(--pumpkin);
   -			--accent: var(--tobacco);
   -			--icon: '🌾';
   -		}
   -
   -		:root[data-theme="medieval"] {
   -			--background: var(--moss);
   -			--text: var(--goat-milk);
   -			--highlight: var(--burl);
   -			--accent: var(--flame);
   -			--icon: '⚔️';
   -			& input[type="text"] {
   -				font-family: BasteleurMoonlight, Arial, sans-serif;
   -			}
   -		}
   -		
   -		:root[data-theme="woodstove"] {
   -			--background: var(--soot);
   -			--text: var(--smoke);
   -			--highlight: var(--ash);
   -			--accent: var(--cinder);
   -			--icon: '🪵';
   -			& input[type="text"] {
   -				font-family: BasteleurMoonlight, Arial, sans-serif;
   -			}
   -			.parent-task {
   -				--text: var(--cinder)
   -			}
   -		}
   -
   -		:root[data-theme="sugar"] {
   -			--background: var(--cola);
   -			--text: var(--taffy);
   -			--highlight: var(--blue-raspberry);
   -			--accent: var(--bubblegum);
   -			--icon: '🍬';
   -
   -			.checkbox-label {
   -				border-radius: 10px;
   -			}
   -			.active {
   -				border-radius: 30px 0px 0px 30px;
   -			}
   -			.custom-checkbox:checked + .checkbox-label::before {
   -				display: none;
   -			}
   -			& input[type="text"] {
   -				text-shadow: 1px 1px 1px var(--cola);
   -			}
   -			input[type="text"]::selection {
   -				color: var(--text);
   -			}
   -		}
   -
   -		:root[data-theme="vineyard"] {
   -			--background: var(--bloom);
   -			--text: var(--cask);
   -			--highlight: var(--muscat);
   -			--accent: var(--muscat);
   -			--icon: '🍇';
   -			& input[type="text"] {
   -				font-family: BasteleurMoonlight;
   -			}
   -			input[type="text"]::selection {
   -				background-color: var(--background);
   -				color: var(--text);
   -				text-shadow: none;
   -			}
   -			.active {
   -				border-radius: 30px 0px 0px 30px;
   -			}
   -			.active input[type="text"] {
   -				color: var(--bloom);
   -				text-shadow: 1px 1px 2px black;
   -			}
   -			.checkbox-label {
   -				border-radius: 50%;
   -				border: none;
   -				background: radial-gradient(circle at 30% 30%, #d4a5d8, #b19cd9, #8a4baf);
   -				box-shadow: 
   -					inset 1px 1px 3px rgba(0,0,0,0.4),
   -					inset -1px -1px 2px rgba(255,255,255,0.3),
   -					0 1px 2px rgba(0,0,0,0.3);
   -			}
   -			.custom-checkbox:checked + .checkbox-label {
   -				background: radial-gradient(circle at 30% 30%, #b19cd9, #8a4baf, #663399);
   -				box-shadow: 
   -					inset -2px -2px 4px rgba(0,0,0,0.3),
   -					inset 1px 1px 2px rgba(255,255,255,0.5),
   -					1px 1px 2px rgba(0,0,0,0.6);
   -			}
   -			.active .custom-checkbox:checked + .checkbox-label {
   -				box-shadow: 
   -					inset -2px -2px 4px rgba(0,0,0,0.3),
   -					inset 1px 1px 2px rgba(255,255,255,0.5),
   -					3px 3px 6px rgba(0,0,0,0.6);
   -			}
   -			.custom-checkbox:checked + .checkbox-label::before {
   -				display: none;
   -			}
   -			.custom-checkbox:indeterminate + .checkbox-label::before {
   -				content: '';
   -				position: absolute;
   -				left: 50%;
   -				top: 50%;
   -				width: 50%;
   -				height: 50%;
   -				border-radius: 50%;
   -				background: radial-gradient(circle at 30% 30%, #b19cd9, #8a4baf, #663399);
   -				box-shadow: 
   -					inset -1px -1px 2px rgba(0,0,0,0.3),
   -					inset 0.5px 0.5px 1px rgba(255,255,255,0.5);
   -				transform: translate(-50%, -50%);
   -			}
   -			scrollbar-color: color-mix(in srgb, var(--muscat) 75%, transparent) color-mix(in srgb, var(--bloom) 75%, transparent);
   -		}
   -
   -		/* interface styling */
   -		@keyframes shake {
   -			0% { transform: translateX(0); }
   -			20% { transform: translateX(3px); }
   -			40% { transform: translateX(-3px); }
   -			60% { transform: translateX(3px); }
   -			80% { transform: translateX(-3px); }
   -			100% { transform: translateX(0); }
   -		}
   -		.shake {
   -			animation: shake 0.25s ease-out;
   -		}
   -		@keyframes shake-vertical {
   -			0% { transform: translateY(0); }
   -			20% { transform: translateY(3px); }
   -			40% { transform: translateY(-3px); }
   -			60% { transform: translateY(3px); }
   -			80% { transform: translateY(-3px); }
   -			100% { transform: translateY(0); }
   -		}
   -		.shake-vertical {
   -			animation: shake-vertical 0.25s ease-out;
   -		}
   -		html {
   -			scroll-behavior: smooth;
   -			overflow-y: scroll;
   -			overflow-x: hidden;
   -			scrollbar-width: auto;
   -			scrollbar-color: color-mix(in srgb, var(--text) 75%, transparent) color-mix(in srgb, var(--background) 75%, transparent);
   -		}
   -		body {
   -			font-family: Arial, sans-serif;
   -			max-width: 800px;
   -			margin: 0 auto;
   -			padding: 0px 20px 20px 20px;
   -			background-color: var(--background);
   -			color: var(--text);
   -		}
   -		ul {
   -			list-style-type: none;
   -			padding-left: 20px;
   -		}
   -		input[type="text"] {
   -			border: none;
   -			background: transparent;
   -			font-size: 1.17em;
   -			font-weight: bold;
   -			width: calc(100% - 30px);
   -			margin-left: 5px;
   -			padding: 5px;
   -			color: var(--text);
   -			overflow: hidden;
   -			text-overflow: ellipsis;
   -			white-space: nowrap;
   -		}
   -		input[type="text"]:focus {
   -			outline: none;
   -		}
   -		input[type="text"]::selection {
   -			background-color: var(--accent);
   -			color: var(--background);
   -		}
   -		.task-container {
   -			display: flex;
   -			align-items: center;
   -			margin: 5px 0;
   -			padding-left: 11px;
   -			padding-top: 4px;
   -			padding-bottom: 4px;
   -		}
   -		.active {
   -			background-color: var(--highlight);
   -		}
   -		.sticky-header {
   -			position: sticky;
   -			top: 0;
   -			background-color: var(--background);
   -			z-index: 1;
   -			padding-top: 20px;
   -		}
   -		.parent-task {
   -			font-size: 1.5em;
   -			font-weight: bold;
   -		}
   -		#breadcrumbs {
   -			font-size: 24px;
   -			margin-bottom: 10px;
   -			color: var(--text);
   -			user-select: none;
   -		}
   -		.custom-checkbox {
   -			display: none;
   -		}
   -		.checkbox-label {
   -			display: inline-block;
   -			width: 20px;
   -			height: 20px;
   -			background-color: var(--background);
   -			border: 2px solid var(--accent);
   -			position: relative;
   -			cursor: pointer;
   -			box-sizing: border-box;
   -			border-radius: 4px;
   -		}
   -		.custom-checkbox:checked + .checkbox-label {
   -			background-color: var(--accent);
   -		}
   -		.custom-checkbox:checked + .checkbox-label::before {
   -			content: '';
   -			position: absolute;
   -			left: 6px;
   -			top: 2px;
   -			width: 5px;
   -			height: 10px;
   -			border: solid var(--background);
   -			border-width: 0 2px 2px 0;
   -			transform: rotate(45deg);
   -			box-sizing: border-box;
   -		}
   -		.custom-checkbox:indeterminate + .checkbox-label::before {
   -			content: '';
   -			position: absolute;
   -			left: 25%;
   -			right: 25%;
   -			top: 50%;
   -			height: 2px;
   -			background-color: var(--accent);
   -			transform: translateY(-50%);
   -		}
   -
   -		/* print styles */
   -		@media print {
   -			body * {
   -				visibility: hidden;
   -			}
   -			#print-content, #print-content * {
   -				visibility: visible;
   -			}
   -			#print-content {
   -				position: absolute;
   -				left: 0;
   -				top: 0;
   -				font-family: monospace;
   -				font-size: 12pt;
   -				line-height: 1.4;
   -				white-space: pre-wrap;
   -				color: black;
   -				background: white;
   -			}
   -			#app-container {
   -				display: none;
   -			}
   -		}
   -	</style>
13 +	<link rel="stylesheet" href="resources/main.css">
14 +	<script src="resources/state.js"></script>
15 +	<script src="resources/storage.js"></script>
16 +	<script src="resources/task-model.js"></script>
17 +	<script src="resources/multi-select.js"></script>
18 +	<script src="resources/navigation.js"></script>
19 +	<script src="resources/reorganize.js"></script>
20 +	<script src="resources/keyboard.js"></script>
21 +	<script src="resources/render.js"></script>
22 +	<script src="resources/task-element.js"></script>
23 +	<script src="resources/theme.js"></script>
24 +	<script src="resources/main.js"></script>
25  	<script>
   -		document.addEventListener('click', function(e) {
   -			if (!e.target.closest('.task-container')) {
   -				const activeTask = document.querySelector('.task-container.active');
   -				if (activeTask) {
   -					const taskInput = activeTask.querySelector('input[type="text"]');
   -					taskInput.focus();
   -				} else {
   -					selectFirstSubtask();
   -				}
   -			}
   -		});
   -		document.addEventListener('DOMContentLoaded', function() {
   -				// Variable to track if we're in a wheel event
   -				let isInWheelEvent = false;
   -			const appContainer = document.getElementById('app-container');
   -			let rootTask = loadTasksFromLocalStorage();
   -			let currentTask = rootTask;
   -			let taskPath = [currentTask];
   -			let lastSubtaskDownArrowReleased = false;
   -			let lastSubtaskShiftDownReleased = false;
   -			let saveTimer = null;
   -			let currentThemeIndex = 0;
   -			let isF2Pressed = false;
   -			let themes = [];
   -
   -			// Multi-select state
   -			let multiSelectAnchorId = null  // task where multi-select started
   -			let multiSelectedIds = []       // ordered array of selected task ids
   -
   -			function clearMultiSelect() {
   -				multiSelectAnchorId = null
   -				multiSelectedIds = []
   -				// Remove .active from non-focused tasks
   -				const focused = document.activeElement
   -				const focusedContainer = focused ? focused.closest('.task-container') : null
   -				document.querySelectorAll('.task-container.active').forEach(el => {
   -					if (el !== focusedContainer) el.classList.remove('active')
   -				})
   -			}
   -
   -			function getMultiSelectedTasks() {
   -				// Return task objects in subtask order
   -				return currentTask.subtasks.filter(t => multiSelectedIds.includes(t.id))
   -			}
   -
   -			function applyMultiSelectHighlights() {
   -				for (const id of multiSelectedIds) {
   -					const container = document.querySelector(`.task-container[data-id="${id}"]`)
   -					if (container) container.classList.add('active')
   -				}
   -			}
   -
   -			function shakeAllSelected(direction = 'horizontal') {
   -				for (const id of multiSelectedIds) {
   -					applyShakeAnimation(id, direction)
   -				}
   -			}
   -
   -			function handleSave(e) {
   -				if ((e.metaKey || e.ctrlKey) && e.key === 's') {
   -					e.preventDefault();
   -					saveTaskTreeToFile();
   -				}
   -			}
   -
   -			function saveTaskTreeToFile() {
   -				const serializedTasks = serializeTaskTree(taskPath[0]);
   -				const rootTaskName = taskPath[0].text || 'Untitled';
   -				const date = new Date();
   -				const fileName = `${rootTaskName} - ${date.toLocaleString('default', { month: 'short' }).toLowerCase()} ${date.getDate()}, '${date.getFullYear().toString().slice(-2)}.txt`;
   -				
   -				const blob = new Blob([serializedTasks], { type: 'text/plain' });
   -				const url = URL.createObjectURL(blob);
   -				
   -				const a = document.createElement('a');
   -				a.href = url;
   -				a.download = fileName;
   -				a.click();
   -				
   -				URL.revokeObjectURL(url);
   -			}
   -
   -			function handleOpen(e) {
   -				if ((e.metaKey || e.ctrlKey) && e.key === 'o') {
   -					e.preventDefault();
   -					openTaskTreeFromFile();
   -				}
   -			}
   -
   -			function openTaskTreeFromFile() {
   -				const input = document.createElement('input');
   -				input.type = 'file';
   -				input.accept = '.txt';
   -				input.onchange = function(event) {
   -					const file = event.target.files[0];
   -					const reader = new FileReader();
   -					reader.onload = function(e) {
   -						try {
   -							const newRootTask = deserializeTaskTree(e.target.result);
   -							if (confirm('Are you sure you want to overwrite the existing task tree?')) {
   -								rootTask = newRootTask;
   -								currentTask = rootTask;
   -								taskPath = [currentTask];
   -								renderCurrentView();
   -								saveTasksToLocalStorage();
   -							}
   -						} catch (error) {
   -							alert(`Error importing task tree: ${error.message}`);
   -						}
   -					};
   -					reader.readAsText(file);
   -				};
   -				input.click();
   -			}
   -
   -			function scheduleSave() {
   -				if (saveTimer) {
   -					clearTimeout(saveTimer);
   -				}
   -				saveTimer = setTimeout(saveTasksToLocalStorage, 1000);
   -			}
   -
   -			function saveTasksToLocalStorage() {
   -				const serializedTasks = serializeTaskTree(taskPath[0]);
   -				localStorage.setItem('taskTree', serializedTasks);
   -				saveTimer = null;
   -			}
   -
   -			function serializeTaskTree(task, depth = 0) {
   -				const indentation = '\t'.repeat(depth);
   -				let status = task.state === 0 ? '_' : (task.state === 1 ? 'x' : '?');
   -				let serialized = `${indentation}${status} ${task.text}\n`;
   -				
   -				for (let subtask of task.subtasks) {
   -					serialized += serializeTaskTree(subtask, depth + 1);
   -				}
   -				
   -				return serialized;
   -			}
   -
   -			function generateId() {
   -				return Date.now().toString(36) + Math.random().toString(36).slice(2)
   -			}
   -
   -			function loadTasksFromLocalStorage() {
   -				const savedTasks = localStorage.getItem('taskTree');
   -				if (savedTasks) {
   -					console.log('%cloaded tasks from local storage:', "color: green;");
   -					console.log(savedTasks);
   -					return deserializeTaskTree(savedTasks);
   -				} else {
   -					return { id: 'root', text: 'todo', state: 0, subtasks: [{ id: generateId(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null };
   -				}
   -			}
   -
   -			function deserializeTaskTree(serialized) {
   -				const lines = serialized.split('\n').filter(line => line.trim() !== '');
   -				const root = { id: 'root', subtasks: [] };
   -				const stack = [{ task: root, depth: -1 }];
   -
   -				for (let line of lines) {
   -					const depth = (line.match(/^\t*/)[0] || '').length;
   -					const status = line[depth];
   -					const text = line.slice(depth + 2);
   -					
   -					const newTask = {
   -						id: depth === 0 ? 'root' : generateId(),
   -						text: text,
   -						state: status === '_' ? 0 : (status === 'x' ? 1 : 2),
   -						subtasks: [],
   -						selectedSubtaskId: null
   -					};
   -					
   -					while (stack.length > 1 && stack[stack.length - 1].depth >= depth) {
   -						stack.pop();
   -					}
   -					
   -					if (depth === 0) {
   -						Object.assign(root, newTask);
   -					} else {
   -						stack[stack.length - 1].task.subtasks.push(newTask);
   -					}
   -					stack.push({ task: newTask, depth: depth });
   -				}
   -				
   -				return root;
   -			}
   -
   -			const keyHandler = {
   -				backspace: {
   -					canDelete: true,
   -					blocked: false
   -				},
   -				enter: {
   -					canAdd: true,
   -					blocked: false
   -				},
   -				arrowDown: {
   -					canAdd: true,
   -					blocked: false
   -				},
   -				shiftArrowDown: {
   -					blocked: false
   -				},
   -				shiftEnter: {
   -					pressed: false
   -				},
   -				shiftRight: {
   -					pressed: false
   -				},
   -				shiftLeft: {
   -					pressed: false
   -				}
   -			};
   -
   -			function createTaskElement(task, isParentTask = false) {
   -				const taskContainer = document.createElement('div');
   -				taskContainer.className = 'task-container';
   -				taskContainer.dataset.id = task.id;
   -				if (isParentTask) taskContainer.classList.add('parent-task');
   -
   -				const checkbox = document.createElement('input');
   -				checkbox.type = 'checkbox';
   -				checkbox.className = 'custom-checkbox';
   -				checkbox.id = `checkbox-${task.id}`;
   -				updateCheckboxState(checkbox, task.state);
   -				checkbox.addEventListener('click', (e) => {
   -					e.preventDefault();
   -					toggleTaskState(task);
   -				});
   -
   -				const checkboxLabel = document.createElement('label');
   -				checkboxLabel.className = 'checkbox-label';
   -				checkboxLabel.setAttribute('for', `checkbox-${task.id}`);
   -
   -				const taskInput = document.createElement('input');
   -				taskInput.type = 'text';
   -				taskInput.value = task.text;
   -				taskInput.setAttribute('autocomplete', 'off');
   -
   -				taskInput.addEventListener('mousedown', function(e) {
   -					e.stopPropagation();
   -				});
   -
   -				const keydownHandler = function(e) {
   -					if (e.key === 'Backspace') {
   -						if (keyHandler.backspace.blocked) {
   -							e.preventDefault();
   -							return;
   -						}
   -						// Multi-select backspace handling
   -						if (multiSelectedIds.length > 1) {
   -							const selected = getMultiSelectedTasks()
   -							const allEmpty = selected.every(t => t.text === '')
   -							if (allEmpty) {
   -								// All empty: require release+repress (canDelete) before bulk deleting
   -								e.preventDefault()
   -								if (keyHandler.backspace.canDelete) {
   -									keyHandler.backspace.blocked = true
   -									const toDelete = selected.filter(t => t !== taskPath[0] && t !== currentTask)
   -									// Don't delete if it would leave the root task with no subtasks
   -									if (toDelete.length > 0 && !(currentTask.id === 'root' && toDelete.length >= currentTask.subtasks.length)) {
   -										// Find the index of the first task to delete (for post-delete selection)
   -										const firstDeleteIdx = Math.min(...toDelete.map(t => currentTask.subtasks.findIndex(s => s.id === t.id)).filter(i => i !== -1))
   -										for (const t of toDelete) {
   -											const idx = currentTask.subtasks.findIndex(s => s.id === t.id)
   -											if (idx !== -1) currentTask.subtasks.splice(idx, 1)
   -										}
   -										clearMultiSelect()
   -										updateTaskAndAncestors(currentTask)
   -										if (currentTask.subtasks.length === 0 && taskPath.length > 1) {
   -											navigateToParentTask()
   -										} else {
   -											renderCurrentView()
   -											const targetIndex = Math.max(0, firstDeleteIdx - 1)
   -											selectAndFocusTask(currentTask.subtasks[targetIndex])
   -										}
   -										scheduleSave()
   -									} else {
   -										// Can't delete: shake all selected tasks
   -										for (const t of selected) {
   -											applyShakeAnimation(t.id)
   -										}
   -									}
   -								}
   -								return
   -							}
   -							// Some tasks still have text: remove last char from all that have text
   -							e.preventDefault()
   -							for (const t of selected) {
   -								if (t.text.length > 0) {
   -									t.text = t.text.slice(0, -1)
   -									const inp = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`)
   -									if (inp) inp.value = t.text
   -								}
   -							}
   -							// Mark canDelete false so when all become empty, user must release+repress
   -							keyHandler.backspace.canDelete = false
   -							scheduleSave()
   -							return
   -						}
   -						if (taskInput.value === '' && keyHandler.backspace.canDelete && multiSelectedIds.length <= 1) {
   -							e.preventDefault();
   -							if (task !== taskPath[0]) {
   -								keyHandler.backspace.blocked = true;
   -								clearMultiSelect()
   -								if (task === currentTask) {
   -									deleteCurrentParentTask();
   -								} else {
   -									deleteSubtask(task);
   -								}
   -							} else {
   -								// Attempting to delete root task
   -								applyShakeAnimation(task.id);
   -							}
   -						} else if (taskInput.value !== '') {
   -							keyHandler.backspace.canDelete = false;
   -						}
   -					} else if (e.key === 'Enter' && !e.shiftKey) {
   -						if (keyHandler.enter.blocked) {
   -							e.preventDefault();
   -							return;
   -						}
   -						if (keyHandler.enter.canAdd) {
   -							e.preventDefault();
   -							keyHandler.enter.blocked = true;
   -							clearMultiSelect()
   -							addNewSubtask(currentTask, task);
   -						}
   -					} else if (e.key === 'ArrowDown' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
   -						if (keyHandler.arrowDown.blocked) {
   -							e.preventDefault();
   -							return;
   -						}
   -						if (isLastSubtask(task) && lastSubtaskDownArrowReleased && task !== currentTask) {
   -							e.preventDefault();
   -							keyHandler.arrowDown.blocked = true;
   -							addNewSubtask(currentTask, task);
   -							lastSubtaskDownArrowReleased = false;
   -						} else {
   -							handleKeyDown(e, task);
   -						}
   -					} else if (e.key === 'ArrowDown' && e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
   -						if (keyHandler.shiftArrowDown.blocked) {
   -							e.preventDefault();
   -							return;
   -						}
   -						// Multi-select: insert new task above chunk when at bottom
   -						if (multiSelectedIds.length > 1 && !e.repeat) {
   -							const selected = getMultiSelectedTasks()
   -							const lastSelected = selected[selected.length - 1]
   -							if (isLastSubtask(lastSelected) && lastSubtaskShiftDownReleased) {
   -								e.preventDefault()
   -								keyHandler.shiftArrowDown.blocked = true
   -								// Find topmost selected task index and insert above chunk
   -								const topIndex = currentTask.subtasks.findIndex(t => multiSelectedIds.includes(t.id))
   -								const newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null }
   -								currentTask.subtasks.splice(topIndex, 0, newSubtask)
   -								updateTaskAndAncestors(currentTask)
   -								currentTask.selectedSubtaskId = multiSelectAnchorId
   -								renderCurrentView()
   -								applyMultiSelectHighlights()
   -								scheduleSave()
   -								lastSubtaskShiftDownReleased = false
   -							} else {
   -								handleKeyDown(e, task)
   -							}
   -						} else if (isLastSubtask(task) && lastSubtaskShiftDownReleased && task !== currentTask && !e.repeat) {
   -							e.preventDefault();
   -							keyHandler.shiftArrowDown.blocked = true;
   -							// Insert a new empty task above the current task, keeping current task selected
   -							const parentTask = findParentTask(task);
   -							if (parentTask) {
   -								const index = parentTask.subtasks.findIndex(t => t.id === task.id);
   -								const newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
   -								parentTask.subtasks.splice(index, 0, newSubtask);
   -								updateTaskAndAncestors(parentTask);
   -								renderCurrentView();
   -								selectAndFocusTask(task);
   -								scheduleSave();
   -							}
   -							lastSubtaskShiftDownReleased = false;
   -						} else {
   -							handleKeyDown(e, task);
   -						}
   -					} else {
   -						handleKeyDown(e, task);
   -					}
   -				};
   -
   -				const keyupHandler = function(e) {
   -					if (e.key === 'Backspace') {
   -						keyHandler.backspace.canDelete = true;
   -						keyHandler.backspace.blocked = false;
   -					} else if (e.key === 'Enter') {
   -						keyHandler.enter.canAdd = true;
   -						keyHandler.enter.blocked = false;
   -						keyHandler.shiftEnter.pressed = false;
   -					} else if (e.key === 'ArrowDown') {
   -						keyHandler.arrowDown.canAdd = true;
   -						keyHandler.arrowDown.blocked = false;
   -						keyHandler.shiftArrowDown.blocked = false;
   -						// During multi-select, check if bottommost selected task is last subtask
   -						const isAtBottom = multiSelectedIds.length > 1
   -							? isLastSubtask(getMultiSelectedTasks().slice(-1)[0])
   -							: isLastSubtask(task)
   -						if (isAtBottom) {
   -							lastSubtaskDownArrowReleased = true;
   -							lastSubtaskShiftDownReleased = true;
   -						} else {
   -							lastSubtaskDownArrowReleased = false;
   -							lastSubtaskShiftDownReleased = false;
   -						}
   -					} else if (e.key === 'ArrowRight') {
   -						keyHandler.shiftRight.pressed = false;
   -					} else if (e.key === 'ArrowLeft') {
   -						keyHandler.shiftLeft.pressed = false;
   -					}
   -				};
   -
   -				taskInput.addEventListener('keydown', keydownHandler);
   -				taskInput.addEventListener('keyup', keyupHandler);
   -				taskInput.addEventListener('keydown', handleCopyAndCut);
   -				taskInput.addEventListener('input', (e) => {
   -					const oldText = task.text
   -					task.text = taskInput.value
   -					// Propagate edits to all other multi-selected tasks (always at end of text)
   -					if (multiSelectedIds.length > 1 && multiSelectedIds.includes(task.id)) {
   -						const otherSelected = getMultiSelectedTasks().filter(t => t.id !== task.id)
   -
   -						if (e.inputType === 'insertText' && e.data) {
   -							// Append typed character(s) to end of each task
   -							for (const t of otherSelected) {
   -								t.text = t.text + e.data
   -								const otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`)
   -								if (otherInput) otherInput.value = t.text
   -							}
   -						} else if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
   -							// Remove last character from each task
   -							for (const t of otherSelected) {
   -								if (t.text.length > 0) {
   -									t.text = t.text.slice(0, -1)
   -								}
   -								const otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`)
   -								if (otherInput) otherInput.value = t.text
   -							}
   -						} else if (e.inputType === 'insertFromPaste' || e.inputType === 'insertFromDrop') {
   -							// Compute what was inserted by comparing old and new text length
   -							const addedLen = taskInput.value.length - oldText.length
   -							if (addedLen > 0) {
   -								const pastedText = taskInput.value.slice(taskInput.selectionStart - addedLen, taskInput.selectionStart)
   -								for (const t of otherSelected) {
   -									t.text = t.text + pastedText
   -									const otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`)
   -									if (otherInput) otherInput.value = t.text
   -								}
   -							}
   -						}
   -					}
   -					if (task === currentTask) {
   -						updatePageTitle(task);
   -					}
   -					scheduleSave();
   -				});
   -
   -				taskInput.addEventListener('focus', () => {
   -					// If clicking a task outside the multi-selection, clear it
   -					if (multiSelectedIds.length > 1 && !multiSelectedIds.includes(task.id)) {
   -						clearMultiSelect()
   -					}
   -					setActiveTask(taskInput, task)
   -				});
   -
   -				taskContainer.appendChild(checkbox);
   -				taskContainer.appendChild(checkboxLabel);
   -				taskContainer.appendChild(taskInput);
   -
   -				return taskContainer;
   -			}
   -
   -			function generateBreadcrumbs(rootTask, currentPath, selectedTaskId) {
   -				let breadcrumbs = '';
   -				let currentTask = rootTask;
   -				let currentDepth = 0;
   -
   -				for (let i = 0; i < currentPath.length - 1; i++) {
   -					breadcrumbs += '○ ';
   -					currentTask = currentTask.subtasks.find(t => t.id === currentPath[i + 1].id);
   -				}
   -
   -				breadcrumbs += '● ';
   -				currentDepth = currentPath.length - 1;
   -
   -				if (selectedTaskId !== currentTask.id) {
   -					let selectedTask = currentTask.subtasks.find(t => t.id === selectedTaskId);
   -					
   -					if (selectedTask) {
   -						function calculateMaxDepth(task, depth) {
   -							if (task.subtasks.length === 0) return depth;
   -							return Math.max(...task.subtasks.map(st => calculateMaxDepth(st, depth + 1)));
   -						}
   -
   -						let maxDepth = calculateMaxDepth(selectedTask, currentDepth + 1);
   -
   -						for (let i = currentDepth + 1; i < maxDepth; i++) {
   -							breadcrumbs += '○ ';
   -						}
   -					}
   -				}
   -
   -				return breadcrumbs.trim();
   -			}
   -
   -			function updateCheckboxState(checkbox, state) {
   -				checkbox.checked = state === 1;
   -				checkbox.indeterminate = state === 2;
   -			}
   -
   -			function toggleTaskState(task) {
   -				// Save cursor position before re-rendering
   -				const taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`)
   -				const cursorPos = taskInput ? taskInput.selectionStart : null
   -				
   -				if (task.state === 1) {
   -					if (task.subtasks.length === 0 || !task.subtasks.every(t => t.state === 1)) {
   -						task.state = 0;
   -						updateSubtasksState(task, 0);
   -						
   -						const parent = findParentTask(task);
   -						if (parent) {
   -							updateTaskAndAncestors(parent);
   -						}
   -						
   -						renderCurrentView();
   -						selectAndFocusTask(task, cursorPos);
   -						scheduleSave();
   -					} else {
   -						// disallow unchecking and play a 'shake' animation
   -						applyShakeAnimation(task.id);
   -					}
   -				} else {
   -					task.state = 1;
   -					updateSubtasksState(task, 1);
   -					
   -					const parent = findParentTask(task);
   -					if (parent) {
   -						updateTaskAndAncestors(parent);
   -					}
   -					
   -					renderCurrentView();
   -					selectAndFocusTask(task, cursorPos);
   -					scheduleSave();
   -				}
   -			}
   -
   -			function applyShakeAnimation(taskId, direction = 'horizontal') {
   -				const checkbox = document.querySelector(`.task-container[data-id="${taskId}"] .checkbox-label`);
   -				if (checkbox) {
   -					const className = direction === 'vertical' ? 'shake-vertical' : 'shake'
   -					if (checkbox.dataset.shaking) return
   -					checkbox.dataset.shaking = '1'
   -					checkbox.classList.add(className);
   -					checkbox.addEventListener('animationend', () => {
   -						checkbox.classList.remove(className)
   -					}, { once: true })
   -					const clearShaking = () => {
   -						delete checkbox.dataset.shaking
   -						document.removeEventListener('keyup', clearShaking)
   -					}
   -					document.addEventListener('keyup', clearShaking)
   -				}
   -			}
   -
   -			function recalculateTaskState(task) {
   -				if (task.subtasks.length === 0) {
   -					return task.state; // If no subtasks, keep current state
   -				}
   -				const anyUnchecked = task.subtasks.some(t => t.state === 0);
   -				const allChecked = task.subtasks.every(t => t.state === 1);
   -				const allCheckedOrIndeterminate = task.subtasks.every(t => t.state === 1 || t.state === 2);
   -
   -				if (allChecked) {
   -					return 1; // All children genuinely checked
   -				} else if (allCheckedOrIndeterminate) {
   -					// Mix of checked and indeterminate (no unchecked)
   -					// Indeterminate parent stays indeterminate; checked/unchecked parent becomes checked
   -					return task.state === 2 ? 2 : 1;
   -				} else if (anyUnchecked) {
   -					return 0; // Unchecked
   -				}
   -			}
   -
   -			function updateTaskAndAncestors(task) {
   -				const newState = recalculateTaskState(task);
   -				if (task.state !== newState) {
   -					const oldState = task.state;
   -					task.state = newState;
   -					
   -					// If the task changed from checked to unchecked, update its descendants
   -					if (oldState === 1 && newState === 0) {
   -						updateSubtasksState(task, 0);
   -					}
   -					
   -					const parent = findParentTask(task);
   -					if (parent) {
   -						updateTaskAndAncestors(parent);
   -					}
   -				}
   -			}
   -
   -			function updateSubtasksState(task, state) {
   -				task.subtasks.forEach(subtask => {
   -					if (subtask.state !== 1) {  // Only update if the subtask is not checked
   -						if (state === 1) {
   -							subtask.state = subtask.state === 0 ? 2 : subtask.state;
   -						} else if (state === 0) {
   -							subtask.state = 0;
   -						}
   -						if (subtask.subtasks.length > 0) {
   -							updateSubtasksState(subtask, state);
   -						}
   -					}
   -				});
   -			}
   -
   -			// Adjust a moved task's state to be valid under its new parent
   -			function adjustMovedTaskState(movedTask, newParent) {
   -				if (movedTask.state === 2 && newParent.state === 0) {
   -					movedTask.state = 0;
   -					updateSubtasksState(movedTask, 0);
   -				}
   -			}
   -
   -			function deleteSubtask(subtask) {
   -				const parentTask = taskPath[taskPath.length - 1];
   -				const index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
   -
   -				if (parentTask.id === 'root' && parentTask.subtasks.length === 1) {
   -					// Disallow deletion of the root task's sole remaining child
   -					applyShakeAnimation(subtask.id);
   -					return;
   -				}
   -
   -				// Delete the subtask
   -				parentTask.subtasks = parentTask.subtasks.filter(t => t.id !== subtask.id);
   -
   -				// Recalculate parent state and propagate changes
   -				updateTaskAndAncestors(parentTask);
   -
   -				if (parentTask.subtasks.length === 0 && taskPath.length > 1) {
   -					navigateToParentTask();
   -				} else {
   -					renderCurrentView();
   -					if (parentTask.subtasks.length > 0) {
   -						const targetIndex = Math.max(0, index - 1);
   -						selectAndFocusTask(parentTask.subtasks[targetIndex]);
   -					} else {
   -						selectAndFocusTask(parentTask);
   -					}
   -				}
   -				scheduleSave();
   -			}
   -
   -			function deleteCurrentParentTask() {
   -				if (taskPath.length <= 1) return; // Don't delete root task
   -
   -				const currentParentTask = taskPath[taskPath.length - 1];
   -				const grandparentTask = taskPath[taskPath.length - 2];
   -
   -				// Check if we're trying to delete the sole child of the root
   -				if (grandparentTask.id === 'root' && grandparentTask.subtasks.length === 1) {
   -					// Don't allow deletion of the sole child of root
   -					applyShakeAnimation(currentParentTask.id);
   -					return;
   -				}
   -
   -				const index = grandparentTask.subtasks.findIndex(t => t.id === currentParentTask.id);
   -
   -				grandparentTask.subtasks = grandparentTask.subtasks.filter(t => t.id !== currentParentTask.id);
   -				updateTaskAndAncestors(grandparentTask);
   -
   -				taskPath.pop(); // Remove the deleted task from the path
   -				currentTask = grandparentTask;
   -
   -				if (grandparentTask.subtasks.length === 0 && taskPath.length > 1) {
   -					// If we've just deleted the last subtask, navigate up another level
   -					navigateToParentTask();
   -				} else {
   -					renderCurrentView();
   -					if (grandparentTask.subtasks.length > 0) {
   -						// Select the subtask before the deleted one, or the one after if at the start
   -						const targetIndex = Math.max(0, index - 1);
   -						selectAndFocusTask(grandparentTask.subtasks[targetIndex]);
   -					} else {
   -						selectAndFocusTask(grandparentTask);
   -					}
   -				}
   -			}
   -
   -			function findParentTask(task) {
   -				for (let i = taskPath.length - 1; i >= 0; i--) {
   -					const potentialParent = taskPath[i];
   -					if (potentialParent.subtasks.some(t => t.id === task.id)) {
   -						return potentialParent;
   -					}
   -				}
   -				return null;
   -			}
   -
   -			// Multi-select: extend selection with Alt+Up/Down
   -			function extendMultiSelect(task, direction) {
   -				if (task === currentTask) return // can't multi-select the parent
   -
   -				const tasks = currentTask.subtasks
   -				if (tasks.length === 0) return
   -
   -				// Initialize multi-select from current task
   -				if (multiSelectAnchorId === null) {
   -					multiSelectAnchorId = task.id
   -					multiSelectedIds = [task.id]
   -				}
   -
   -				const anchorIndex = tasks.findIndex(t => t.id === multiSelectAnchorId)
   -				const currentIndex = tasks.findIndex(t => t.id === task.id)
   -
   -				if (direction === 'down') {
   -					// Find the bottommost selected task
   -					const bottomIndex = tasks.findIndex((t, i) => {
   -						return multiSelectedIds.includes(t.id) &&
   -							(i === tasks.length - 1 || !multiSelectedIds.includes(tasks[i + 1].id))
   -					})
   -					if (currentIndex <= anchorIndex && multiSelectedIds.length > 1) {
   -						// Contracting: remove from top
   -						multiSelectedIds = multiSelectedIds.filter(id => id !== task.id)
   -						const nextTask = tasks[currentIndex + 1]
   -						if (nextTask) selectAndFocusTask(nextTask)
   -					} else if (bottomIndex < tasks.length - 1) {
   -						// Extending downward
   -						const nextTask = tasks[bottomIndex + 1]
   -						multiSelectedIds.push(nextTask.id)
   -						selectAndFocusTask(nextTask)
   -					}
   -				} else {
   -					// Find the topmost selected task
   -					const topIndex = tasks.findIndex(t => multiSelectedIds.includes(t.id))
   -					if (currentIndex >= anchorIndex && multiSelectedIds.length > 1) {
   -						// Contracting: remove from bottom
   -						multiSelectedIds = multiSelectedIds.filter(id => id !== task.id)
   -						const prevTask = tasks[currentIndex - 1]
   -						if (prevTask) selectAndFocusTask(prevTask)
   -					} else if (topIndex > 0) {
   -						// Extending upward
   -						const prevTask = tasks[topIndex - 1]
   -						multiSelectedIds.unshift(prevTask.id)
   -						selectAndFocusTask(prevTask)
   -					}
   -				}
   -
   -				applyMultiSelectHighlights()
   -			}
   -
   -			function handleKeyDown(e, task) {
   -				const cmd = e.metaKey || e.ctrlKey;
   -				const hasMultiSelect = multiSelectedIds.length > 1
   -
   -				// Alt+Up/Down: extend multi-selection
   -				if (e.key === 'ArrowUp' && e.altKey && !cmd && !e.shiftKey) {
   -					e.preventDefault();
   -					extendMultiSelect(task, 'up')
   -					return
   -				} else if (e.key === 'ArrowDown' && e.altKey && !cmd && !e.shiftKey) {
   -					e.preventDefault();
   -					extendMultiSelect(task, 'down')
   -					return
   -				}
   -
   -				// Shift+Enter: toggle task state (bulk if multi-selected)
   -				if (e.key === 'Enter' && e.shiftKey) {
   -					e.preventDefault();
   -					if (!keyHandler.shiftEnter.pressed) {
   -						keyHandler.shiftEnter.pressed = true;
   -						if (hasMultiSelect) {
   -							bulkToggleTaskState()
   -						} else {
   -							toggleTaskState(task);
   -						}
   -					}
   -				// Cmd+Up: push into task above (bulk if multi-selected)
   -				} else if (e.key === 'ArrowUp' && cmd && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pushMultiSelectedIntoTarget('up') : pushSubtaskIntoTarget(task, 'up')
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical')
   -						}
   -					}
   -				// Cmd+Down: push into task below (bulk if multi-selected)
   -				} else if (e.key === 'ArrowDown' && cmd && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pushMultiSelectedIntoTarget('down') : pushSubtaskIntoTarget(task, 'down')
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical')
   -						}
   -					}
   -				} else if (e.key === 'ArrowRight' && cmd && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
   -				// Cmd+Left: pull out one level (bulk if multi-selected)
   -				} else if (e.key === 'ArrowLeft' && cmd && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pullMultiSelectedOutLayer() : pullSubtaskOutLayer(task)
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id)
   -						}
   -					}
   -				// Cmd+Shift+Up: push and navigate (bulk if multi-selected)
   -				} else if (e.key === 'ArrowUp' && cmd && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pushMultiSelectedIntoTarget('up', true) : pushSubtaskIntoTarget(task, 'up', true)
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical')
   -						}
   -					}
   -				// Cmd+Shift+Down: push and navigate (bulk if multi-selected)
   -				} else if (e.key === 'ArrowDown' && cmd && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pushMultiSelectedIntoTarget('down', true) : pushSubtaskIntoTarget(task, 'down', true)
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical')
   -						}
   -					}
   -				// Cmd+Shift+Left: pull and navigate (bulk if multi-selected)
   -				} else if (e.key === 'ArrowLeft' && cmd && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (!e.repeat) {
   -						const success = hasMultiSelect ? pullMultiSelectedOutLayer(true) : pullSubtaskOutLayer(task, true)
   -						if (!success) {
   -							hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id)
   -						}
   -					}
   -				} else if (e.key === 'ArrowRight' && cmd && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
   -				// Plain Up/Down: navigate (clears multi-select)
   -				} else if (e.key === 'ArrowUp' && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect) clearMultiSelect()
   -					navigateTasks('up');
   -				} else if (e.key === 'ArrowDown' && !e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect) clearMultiSelect()
   -					navigateTasks('down');
   -				// Shift+Up/Down: move task (bulk if multi-selected)
   -				} else if (e.key === 'ArrowUp' && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect) {
   -						if (!moveMultiSelected('up')) shakeAllSelected('vertical')
   -					} else {
   -						if (!moveSubtask(task, 'up')) applyShakeAnimation(task.id, 'vertical')
   -					}
   -				} else if (e.key === 'ArrowDown' && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect) {
   -						if (!moveMultiSelected('down')) shakeAllSelected('vertical')
   -					} else {
   -						if (!moveSubtask(task, 'down')) applyShakeAnimation(task.id, 'vertical')
   -					}
   -				// Shift+Right: blocked during multi-select, shake all
   -				} else if (e.key === 'ArrowRight' && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect) {
   -						if (!keyHandler.shiftRight.pressed) {
   -							keyHandler.shiftRight.pressed = true
   -							shakeAllSelected()
   -						}
   -					} else if (!keyHandler.shiftRight.pressed) {
   -						keyHandler.shiftRight.pressed = true;
   -						if (task !== currentTask) {
   -							navigateIntoSubtask(task);
   -						} else {
   -							applyShakeAnimation(task.id);
   -						}
   -					}
   -				// Shift+Left: during multi-select, shake at root or clear and navigate out
   -				} else if (e.key === 'ArrowLeft' && e.shiftKey && !e.altKey) {
   -					e.preventDefault();
   -					if (hasMultiSelect && currentTask.id === 'root') {
   -						if (!keyHandler.shiftLeft.pressed) {
   -							keyHandler.shiftLeft.pressed = true
   -							shakeAllSelected()
   -						}
   -					} else if (hasMultiSelect) {
   -						clearMultiSelect()
   -						if (!keyHandler.shiftLeft.pressed) {
   -							keyHandler.shiftLeft.pressed = true
   -							navigateToParentTask()
   -						}
   -					} else if (!keyHandler.shiftLeft.pressed) {
   -						keyHandler.shiftLeft.pressed = true;
   -						navigateToParentTask();
   -					}
   -				}
   -			}
   -
   -			function moveSubtask(subtask, direction) {
   -				if (subtask === currentTask) return false;
   -
   -				const parentTask = findParentTask(subtask);
   -				if (!parentTask) return false;
   -
   -				const index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
   -				if (index === -1) return false;
   -
   -				if (direction === 'up' && index > 0) {
   -					[parentTask.subtasks[index - 1], parentTask.subtasks[index]] = [parentTask.subtasks[index], parentTask.subtasks[index - 1]];
   -				} else if (direction === 'down' && index < parentTask.subtasks.length - 1) {
   -					[parentTask.subtasks[index], parentTask.subtasks[index + 1]] = [parentTask.subtasks[index + 1], parentTask.subtasks[index]];
   -				} else {
   -					return false;
   -				}
   -
   -				renderCurrentView();
   -				selectAndFocusTask(subtask);
   -				scheduleSave();
   -				return true;
   -			}
   -
   -			function pushSubtaskIntoTarget(subtask, direction, navigate = false) {
   -				// Don't allow pushing the current parent task
   -				if (subtask === currentTask) return false;
   -
   -				if (navigate) {
   -					document.documentElement.style.scrollBehavior = 'auto';
   -				}
   -
   -				const parentTask = findParentTask(subtask);
   -				if (!parentTask) return false;
   -
   -				const index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
   -				if (index === -1) return false;
   -
   -				let targetTask = null;
   -				if (direction === 'up' && index > 0) {
   -					targetTask = parentTask.subtasks[index - 1];
   -				} else if (direction === 'down' && index < parentTask.subtasks.length - 1) {
   -					targetTask = parentTask.subtasks[index + 1];
   -				}
   -
   -				if (!targetTask) return false;
   -
   -				// Remove the subtask from its current parent
   -				parentTask.subtasks.splice(index, 1);
   -
   -				// Add the subtask to the top of the target task's subtasks
   -				targetTask.subtasks.unshift(subtask);
   -
   -				// Mark the pushed task as selected within the target
   -				targetTask.selectedSubtaskId = subtask.id;
   -
   -				// Adjust moved task's state for its new parent
   -				adjustMovedTaskState(subtask, targetTask);
   -
   -				// Update parent task state after removal
   -				updateTaskAndAncestors(parentTask);
   -
   -				// Update target task state after addition
   -				updateTaskAndAncestors(targetTask);
   -
   -				if (navigate) {
   -					navigateIntoTaskAndSelectSubtask(targetTask, subtask);
   -				} else {
   -					renderCurrentView();
   -					selectAndFocusTask(targetTask);
   -				}
   -
   -				scheduleSave();
   -				return true;
   -			}
   -
   -			function pullSubtaskOutLayer(subtask, navigate = false) {
   -				// Don't allow pulling the current parent task
   -				if (subtask === currentTask) return false;
   -
   -				// Don't allow pulling when at root level
   -				if (taskPath.length <= 1) return false;
   -
   -				if (navigate) {
   -					document.documentElement.style.scrollBehavior = 'auto';
   -				}
   -
   -				const currentParent = findParentTask(subtask);
   -				if (!currentParent) return;
   -
   -				// Find the grandparent (the outer layer we're pulling into)
   -				const grandParent = findParentTask(currentParent);
   -				if (!grandParent) return;
   -
   -				// Remove the subtask from its current parent
   -				const currentIndex = currentParent.subtasks.findIndex(t => t.id === subtask.id);
   -				if (currentIndex === -1) return;
   -				currentParent.subtasks.splice(currentIndex, 1);
   -
   -				// Find where to insert in the grandparent's subtasks
   -				const currentParentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id);
   -				if (currentParentIndex === -1) {
   -					// Fallback: add to end of grandparent's subtasks
   -					grandParent.subtasks.push(subtask);
   -				} else {
   -					// Insert after the current parent's position
   -					grandParent.subtasks.splice(currentParentIndex + 1, 0, subtask);
   -				}
   -
   -				// Adjust moved task's state for its new parent
   -				adjustMovedTaskState(subtask, grandParent);
   -
   -				// Update task states
   -				updateTaskAndAncestors(currentParent);
   -				updateTaskAndAncestors(grandParent);
   -
   -				if (navigate || (currentParent.subtasks.length === 0 && taskPath.length > 1)) {
   -					// Navigate to parent level and select the moved task
   -					navigateToParentTaskAndSelectTask(subtask);
   -				} else {
   -					// Stay at current level and select adjacent task
   -					renderCurrentView();
   -					if (currentParent.subtasks.length > 0) {
   -						const targetIndex = Math.max(0, currentIndex - 1);
   -						selectAndFocusTask(currentParent.subtasks[targetIndex]);
   -					} else {
   -						selectAndFocusTask(currentParent);
   -					}
   -				}
   -
   -				scheduleSave();
   -				return true;
   -			}
   -
   -			// Bulk move: shift entire multi-selected chunk up or down
   -			function moveMultiSelected(direction) {
   -				const selected = getMultiSelectedTasks()
   -				if (selected.length === 0) return false
   -
   -				const subtasks = currentTask.subtasks
   -				const indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b)
   -				const topIndex = indices[0]
   -				const bottomIndex = indices[indices.length - 1]
   -
   -				if (direction === 'up' && topIndex <= 0) return false
   -				if (direction === 'down' && bottomIndex >= subtasks.length - 1) return false
   -
   -				// Remove the chunk
   -				const chunk = subtasks.splice(topIndex, selected.length)
   -				// Re-insert shifted by one
   -				const insertAt = direction === 'up' ? topIndex - 1 : topIndex + 1
   -				subtasks.splice(insertAt, 0, ...chunk)
   -
   -				// Ensure render focuses a multi-selected task
   -				currentTask.selectedSubtaskId = multiSelectAnchorId
   -				renderCurrentView()
   -				applyMultiSelectHighlights()
   -				scheduleSave()
   -				return true
   -			}
   -
   -			// Bulk push: push entire multi-selected chunk into adjacent task
   -			function pushMultiSelectedIntoTarget(direction, navigate = false) {
   -				const selected = getMultiSelectedTasks()
   -				if (selected.length === 0) return false
   -
   -				if (navigate) {
   -					document.documentElement.style.scrollBehavior = 'auto'
   -				}
   -
   -				const subtasks = currentTask.subtasks
   -				const indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b)
   -				const topIndex = indices[0]
   -				const bottomIndex = indices[indices.length - 1]
   -
   -				// Find target: the task adjacent to the chunk that is NOT in the selection
   -				let targetTask = null
   -				if (direction === 'up' && topIndex > 0) {
   -					targetTask = subtasks[topIndex - 1]
   -				} else if (direction === 'down' && bottomIndex < subtasks.length - 1) {
   -					targetTask = subtasks[bottomIndex + 1]
   -				}
   -				if (!targetTask) return false
   -
   -				// Remove selected tasks from current parent, keeping target
   -				const chunk = selected.slice()
   -				currentTask.subtasks = currentTask.subtasks.filter(t => !multiSelectedIds.includes(t.id))
   -
   -				// Add chunk to target's subtasks (at the top, preserving order)
   -				targetTask.subtasks.unshift(...chunk)
   -
   -				// Adjust states for moved tasks
   -				for (const t of chunk) {
   -					adjustMovedTaskState(t, targetTask)
   -				}
   -				targetTask.selectedSubtaskId = multiSelectAnchorId
   -				updateTaskAndAncestors(currentTask)
   -				updateTaskAndAncestors(targetTask)
   -
   -				if (navigate) {
   -					// Navigate into target, keep multi-select
   -					currentTask.selectedSubtaskId = targetTask.id
   -					taskPath.push(targetTask)
   -					currentTask = targetTask
   -					updateBreadcrumbs(currentTask)
   -					renderCurrentView()
   -					applyMultiSelectHighlights()
   -				} else {
   -					// Stay at current level, chunk is no longer visible
   -					clearMultiSelect()
   -					renderCurrentView()
   -					selectAndFocusTask(targetTask)
   -				}
   -
   -				scheduleSave()
   -				return true
   -			}
   -
   -			// Bulk pull: pull entire multi-selected chunk out one level
   -			function pullMultiSelectedOutLayer(navigate = false) {
   -				if (taskPath.length <= 1) return false
   -
   -				const selected = getMultiSelectedTasks()
   -				if (selected.length === 0) return false
   -
   -				if (navigate) {
   -					document.documentElement.style.scrollBehavior = 'auto'
   -				}
   -
   -				const currentParent = currentTask
   -				const grandParent = taskPath[taskPath.length - 2]
   -				if (!grandParent) return false
   -
   -				// Remove chunk from current parent
   -				currentParent.subtasks = currentParent.subtasks.filter(t => !multiSelectedIds.includes(t.id))
   -
   -				// Insert after current parent in grandparent's subtasks, preserving order
   -				const parentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id)
   -				if (parentIndex === -1) {
   -					grandParent.subtasks.push(...selected)
   -				} else {
   -					grandParent.subtasks.splice(parentIndex + 1, 0, ...selected)
   -				}
   -
   -				// Adjust states for moved tasks
   -				for (const t of selected) {
   -					adjustMovedTaskState(t, grandParent)
   -				}
   -
   -				updateTaskAndAncestors(currentParent)
   -				updateTaskAndAncestors(grandParent)
   -
   -				if (navigate || currentParent.subtasks.length === 0) {
   -					// Navigate out to parent level, keep multi-select
   -					taskPath.pop()
   -					currentTask = taskPath[taskPath.length - 1]
   -					// Set selectedSubtaskId to anchor so renderCurrentView focuses a multi-selected task
   -					currentTask.selectedSubtaskId = multiSelectAnchorId
   -					updateBreadcrumbs(currentTask)
   -					renderCurrentView()
   -					applyMultiSelectHighlights()
   -				} else {
   -					// Stay at current level, chunk is gone
   -					clearMultiSelect()
   -					renderCurrentView()
   -					if (currentParent.subtasks.length > 0) {
   -						selectAndFocusTask(currentParent.subtasks[0])
   -					} else {
   -						selectAndFocusTask(currentParent)
   -					}
   -				}
   -
   -				scheduleSave()
   -				return true
   -			}
   -
   -			// Bulk toggle: uniform toggle for multi-selected tasks
   -			function bulkToggleTaskState() {
   -				const selected = getMultiSelectedTasks()
   -				if (selected.length === 0) return
   -
   -				// If any are unchecked (state !== 1), check all. Otherwise uncheck all.
   -				const anyUnchecked = selected.some(t => t.state !== 1)
   -				const newState = anyUnchecked ? 1 : 0
   -
   -				for (const t of selected) {
   -					t.state = newState
   -					// Update subtask states recursively
   -					function setAllSubtasks(task, state) {
   -						task.state = state
   -						for (const sub of task.subtasks) {
   -							setAllSubtasks(sub, state)
   -						}
   -					}
   -					setAllSubtasks(t, newState)
   -				}
   -
   -				updateTaskAndAncestors(currentTask)
   -				currentTask.selectedSubtaskId = multiSelectAnchorId
   -				renderCurrentView()
   -				applyMultiSelectHighlights()
   -				scheduleSave()
   -			}
   -
   -			function addNewSubtask(parentTask, currentSubtask = null) {
   -				const newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
   -				if (currentSubtask) {
   -					const index = parentTask.subtasks.findIndex(t => t.id === currentSubtask.id);
   -					parentTask.subtasks.splice(index + 1, 0, newSubtask);
   -				} else {
   -					parentTask.subtasks.push(newSubtask);
   -				}
   -
   -				updateTaskAndAncestors(parentTask);
   -
   -				renderCurrentView();
   -				selectAndFocusTask(newSubtask);
   -				scheduleSave();
   -			}
   -
   -			function selectAndFocusTask(task, cursorPos) {
   -				const taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
   -				if (taskInput) {
   -					taskInput.focus();
   -					const pos = cursorPos != null ? cursorPos : taskInput.value.length
   -					taskInput.setSelectionRange(pos, pos);
   -					setActiveTask(taskInput, task);
   -				}
   -			}
   -
   -			function placeCursorAtBeginning(input) {
   -				input.setSelectionRange(0, 0);
   -			}
   -
   -			function navigateTasks(direction) {
   -				const tasks = currentTask.subtasks;
   -				const subtaskIndex = tasks.findIndex(t => t.id === currentTask.selectedSubtaskId);
   -
   -				// Check if the parent task itself is focused
   -				const currentElement = document.activeElement;
   -				const currentContainer = currentElement ? currentElement.closest('.task-container') : null;
   -				const isParentFocused = currentContainer && currentContainer.dataset.id == currentTask.id;
   -
   -				if (direction === 'up') {
   -					if (isParentFocused) {
   -						// Already at parent, can't go higher
   -					} else if (subtaskIndex <= 0) {
   -						// At first subtask or not found, go to parent
   -						selectAndFocusTask(currentTask);
   -					} else {
   -						selectAndFocusTask(tasks[subtaskIndex - 1]);
   -					}
   -				} else {
   -					if (isParentFocused) {
   -						// Move from parent to first subtask
   -						if (tasks.length > 0) {
   -							selectAndFocusTask(tasks[0]);
   -						}
   -					} else if (subtaskIndex >= 0 && subtaskIndex < tasks.length - 1) {
   -						selectAndFocusTask(tasks[subtaskIndex + 1]);
   -					}
   -				}
   -				lastSubtaskDownArrowReleased = false;
   -				lastSubtaskShiftDownReleased = false;
   -			}
   -
   -			function navigateIntoSubtask(subtask) {
   -				// Temporarily disable smooth scrolling
   -				document.documentElement.style.scrollBehavior = 'auto';
   -				
   -				if (subtask.subtasks.length > 0) {
   -					currentTask.selectedSubtaskId = subtask.id;
   -					taskPath.push(subtask);
   -					currentTask = subtask;
   -					updateBreadcrumbs(currentTask);
   -					renderCurrentView();
   -					const selectedSubtask = subtask.selectedSubtaskId ? 
   -						subtask.subtasks.find(t => t.id === subtask.selectedSubtaskId) : 
   -						subtask.subtasks[0];
   -					selectAndFocusTask(selectedSubtask);
   -				} else {
   -					addNewSubtask(subtask);
   -					currentTask.selectedSubtaskId = subtask.id;
   -					taskPath.push(subtask);
   -					currentTask = subtask;
   -					updateBreadcrumbs(currentTask);
   -					renderCurrentView();
   -					selectAndFocusTask(subtask.subtasks[0]);
   -				}
   -			}
   -
   -			function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) {
   -				// Temporarily disable smooth scrolling
   -				document.documentElement.style.scrollBehavior = 'auto';
   -				
   -				// Set the selectedSubtaskId on the current task before navigating
   -				currentTask.selectedSubtaskId = targetTask.id;
   -				
   -				// Navigate into the target task
   -				taskPath.push(targetTask);
   -				currentTask = targetTask;
   -				updateBreadcrumbs(currentTask);
   -				renderCurrentView();
   -				
   -				// Select the moved subtask
   -				selectAndFocusTask(subtaskToSelect);
   -				currentTask.selectedSubtaskId = subtaskToSelect.id;
   -			}
   -
   -			function navigateToParentTask() {
   -				// Check if the current active parent task is the root
   -				if (currentTask.id === 'root') {
   -					// We're at root level - cannot navigate out, apply shake animation to active task
   -					const activeTaskElement = document.querySelector('.task-container.active');
   -					if (activeTaskElement) {
   -						const taskId = activeTaskElement.dataset.id;
   -						applyShakeAnimation(taskId);
   -					}
   -				} else if (taskPath.length > 1) {
   -					// Normal navigation out - we're deeper than root level
   -					// Temporarily disable smooth scrolling
   -					document.documentElement.style.scrollBehavior = 'auto';
   -					
   -					const currentTaskId = currentTask.id;
   -					taskPath.pop();
   -					currentTask = taskPath[taskPath.length - 1];
   -					updateBreadcrumbs(currentTask);
   -					renderCurrentView();
   -					const selectedSubtask = currentTask.subtasks.find(t => t.id === currentTaskId);
   -					if (selectedSubtask) {
   -						selectAndFocusTask(selectedSubtask);
   -					} else if (currentTask.subtasks.length > 0) {
   -						selectAndFocusTask(currentTask.subtasks[0]);
   -					} else {
   -						selectAndFocusTask(currentTask);
   -					}
   -					currentTask.selectedSubtaskId = currentTaskId;
   -				}
   -			}
   -
   -			function navigateToParentTaskAndSelectTask(targetTask) {
   -				// Check if the current active parent task is the root
   -				if (currentTask.id === 'root') {
   -					// We're at root level, just re-render and select the target task
   -					renderCurrentView();
   -					selectAndFocusTask(targetTask);
   -				} else if (taskPath.length > 1) {
   -					// Normal navigation out - we're deeper than root level
   -					// Temporarily disable smooth scrolling
   -					document.documentElement.style.scrollBehavior = 'auto';
   -					
   -					taskPath.pop();
   -					currentTask = taskPath[taskPath.length - 1];
   -					updateBreadcrumbs(currentTask);
   -					renderCurrentView();
   -					selectAndFocusTask(targetTask);
   -					currentTask.selectedSubtaskId = targetTask.id;
   -				}
   -			}
   -
   -			function setActiveTask(input, task) {
   -				document.querySelectorAll('.active').forEach(el => el.classList.remove('active'));
   -				input.closest('.task-container').classList.add('active');
   -				if (task !== currentTask) {
   -					currentTask.selectedSubtaskId = task.id;
   -				}
   -				// Re-apply multi-select highlights after clearing
   -				if (multiSelectedIds.length > 1) {
   -					applyMultiSelectHighlights()
   -				}
   -				updateBreadcrumbs(task);
   -				// During multi-select, check bottommost selected task for "last subtask" status
   -				const bottomTask = multiSelectedIds.length > 1
   -					? getMultiSelectedTasks().slice(-1)[0]
   -					: task
   -				lastSubtaskDownArrowReleased = isLastSubtask(bottomTask);
   -				lastSubtaskShiftDownReleased = isLastSubtask(bottomTask);
   -				input.focus();
   -
   -				// Center the active task in the viewport
   -				const activeTaskElement = input.closest('.task-container');
   -				if (activeTaskElement && activeTaskElement.parentElement && activeTaskElement.parentElement.tagName === 'LI') {
   -					// Use scrollIntoView with block: "center" to center the element vertically
   -					activeTaskElement.scrollIntoView({
   -						behavior: 'auto',  // Use 'auto' for immediate scrolling
   -						block: 'center',   // Center vertically
   -						inline: 'nearest'  // Don't change horizontal scroll
   -					});
   -
   -					// Re-enable smooth scrolling after all DOM updates and scrolling are complete
   -					setTimeout(() => {
   -						document.documentElement.style.scrollBehavior = 'smooth';
   -					}, 200);
   -				}
   -			}
   -
   -			function updateBreadcrumbs(selectedTask) {
   -				const breadcrumbsContainer = document.getElementById('breadcrumbs');
   -				// During multi-select, show breadcrumbs as if parent is selected (no deeper traversal)
   -				const effectiveId = multiSelectedIds.length > 1 ? currentTask.id : selectedTask.id;
   -				const trail = generateBreadcrumbs(taskPath[0], taskPath, effectiveId);
   -				breadcrumbsContainer.textContent = trail;
   -			}
   -
   -			function renderCurrentView() {
   -				appContainer.innerHTML = '';
   -				currentTask = taskPath[taskPath.length - 1];
   -
   -				const stickyHeader = document.createElement('div');
   -				stickyHeader.className = 'sticky-header';
   -
   -				const breadcrumbsElement = document.createElement('div');
   -				breadcrumbsElement.id = 'breadcrumbs';
   -				stickyHeader.appendChild(breadcrumbsElement);
   -
   -				const parentElement = createTaskElement(currentTask, true);
   -				stickyHeader.appendChild(parentElement);
   -
   -				appContainer.appendChild(stickyHeader);
   -
   -				const subtasksContainer = document.createElement('div');
   -				subtasksContainer.id = 'subtasks-container';
   -
   -				const subtasksList = document.createElement('ul');
   -				currentTask.subtasks.forEach(subtask => {
   -					const li = document.createElement('li');
   -					li.appendChild(createTaskElement(subtask));
   -					subtasksList.appendChild(li);
   -				});
   -				subtasksContainer.appendChild(subtasksList);
   -				appContainer.appendChild(subtasksContainer);
   -
   -				updateBreadcrumbs(currentTask);
   -				updatePageTitle(currentTask);
   -
   -				const parentCheckbox = parentElement.querySelector('input[type="checkbox"]');
   -				updateCheckboxState(parentCheckbox, currentTask.state);
   -
   -				if (currentTask.selectedSubtaskId) {
   -					const selectedTask = currentTask.subtasks.find(t => t.id === currentTask.selectedSubtaskId);
   -					if (selectedTask) {
   -						selectAndFocusTask(selectedTask);
   -					} else {
   -						selectFirstSubtask();
   -					}
   -				} else {
   -					selectFirstSubtask();
   -				}
   -			}
   -
   -			function selectFirstSubtask() {
   -				if (currentTask.subtasks.length > 0) {
   -					const firstSubtask = currentTask.subtasks[0];
   -					selectAndFocusTask(firstSubtask);
   -				} else {
   -					selectAndFocusTask(currentTask);
   -				}
   -			}
   -
   -			function handleCopyAndCut(e) {
   -				if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'x')) {
   -					const activeTaskInput = document.querySelector('.task-container.active input[type="text"]');
   -					if (activeTaskInput) {
   -						e.preventDefault();
   -
   -						// If there's no selection, select all text
   -						if (activeTaskInput.selectionStart === activeTaskInput.selectionEnd) {
   -							activeTaskInput.select();
   -						}
   -
   -						// Use execCommand for copy or cut
   -						if (e.key === 'c') {
   -							document.execCommand('copy');
   -						} else if (e.key === 'x') {
   -							document.execCommand('cut');
   -
   -							// Update the task object
   -							const taskContainer = activeTaskInput.closest('.task-container');
   -							const taskId = taskContainer.dataset.id;
   -							const task = currentTask.id === taskId ? currentTask : currentTask.subtasks.find(t => t.id === taskId);
   -							if (task) {
   -								task.text = activeTaskInput.value;
   -								scheduleSave();
   -							}
   -						}
   -
   -						// Deselect text if we had to select all
   -						if (e.key === 'c' && activeTaskInput.selectionStart === 0 && activeTaskInput.selectionEnd === activeTaskInput.value.length) {
   -							activeTaskInput.setSelectionRange(activeTaskInput.value.length, activeTaskInput.value.length);
   -						}
   -					}
   -				}
   -			}
   -
   -			function updatePageTitle(task) {
   -				document.title = task.text || '?';
   -			}
   -
   -			function isLastSubtask(task) {
   -				const parentTask = findParentTask(task);
   -				if (!parentTask) return false;
   -				return parentTask.subtasks[parentTask.subtasks.length - 1].id === task.id;
   -			}
   -
   -			function getThemesFromCSS() {
   -				const styleElement = document.querySelector('style');
   -				if (styleElement && styleElement.textContent) {
   -					const cssText = styleElement.textContent;
   -					const themeRegex = /:root\[data-theme="([^"]+)"\]/g;
   -					let match;
   -					while ((match = themeRegex.exec(cssText)) !== null) {
   -						themes.push(match[1]);
   -					}
   -				}
   -			}
   -
   -			function cycleTheme() {
   -				currentThemeIndex = (currentThemeIndex + 1) % themes.length;
   -				const newTheme = themes[currentThemeIndex];
   -				setTheme(newTheme);
   -				saveThemeToLocalStorage(newTheme);
   -			}
   -
   -			function saveThemeToLocalStorage(theme) {
   -				localStorage.setItem('currentTheme', theme);
   -			}
   -
   -			function updateFavicon() {
   -				const iconEmoji = getComputedStyle(document.documentElement).getPropertyValue('--icon').trim().replace(/'/g, '');
   -				const faviconLink = document.querySelector('link[rel="icon"]');
   -				if (faviconLink && iconEmoji) {
   -					faviconLink.href = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${iconEmoji}</text></svg>`;
   -				}
   -			}
   -
   -			function setTheme(theme) {
   -				document.documentElement.setAttribute('data-theme', theme);
   -				const backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
   -				document.getElementById('themeColor').setAttribute('content', backgroundColor);
   -				updateFavicon();
   -			}
   -
   -			function setInitialTheme() {
   -				const savedTheme = localStorage.getItem('currentTheme');
   -				const defaultTheme = document.documentElement.getAttribute('data-theme');
   -				
   -				if (savedTheme && themes.includes(savedTheme)) {
   -					currentThemeIndex = themes.indexOf(savedTheme);
   -				} else if (themes.includes(defaultTheme)) {
   -					currentThemeIndex = themes.indexOf(defaultTheme);
   -				} else {
   -					currentThemeIndex = 0;
   -				}
   -				
   -				setTheme(themes[currentThemeIndex]);
   -			}
   -
   -			document.addEventListener('keydown', function(event) {
   -				if (event.key === 'F2' && !isF2Pressed) {
   -					event.preventDefault();
   -					isF2Pressed = true;
   -					cycleTheme();
   -				}
   -			});
   -
   -			document.addEventListener('keyup', function(event) {
   -				if (event.key === 'F2') {
   -					isF2Pressed = false;
   -				}
   -			});
   -
   -			document.addEventListener('keydown', handleSave);
   -			document.addEventListener('keydown', handleOpen);
   -			
   -			// Add scroll handler to block default scroll behavior
   -			window.addEventListener('wheel', handleScroll, { passive: false });
   -
   -			// Add print handler
   -			window.addEventListener('beforeprint', handleBeforePrint);
   -			window.addEventListener('afterprint', handleAfterPrint);
   -			
   -			function handleBeforePrint() {
   -				// Create print content div if it doesn't exist
   -				let printContent = document.getElementById('print-content');
   -				if (!printContent) {
   -					printContent = document.createElement('div');
   -					printContent.id = 'print-content';
   -					document.body.appendChild(printContent);
   -				}
   -				
   -				// Generate the serialized task tree for current parent task and its descendants
   -				const serializedTasks = serializeTaskTree(currentTask);
   -				printContent.textContent = serializedTasks;
   -			}
   -
   -			function handleAfterPrint() {
   -				// Clean up print content
   -				const printContent = document.getElementById('print-content');
   -				if (printContent) {
   -					printContent.remove();
   -				}
   -			}
   -
   -			function handleScroll(e) {
   -				// Prevent the default scroll behavior
   -				e.preventDefault();
   -			}
   -
   -			// Register focusin handler once (not per-element) to reset cursor on non-focused inputs
   -			appContainer.addEventListener('focusin', function(e) {
   -				if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
   -					document.querySelectorAll('input[type="text"]').forEach(input => {
   -						if (input !== e.target) {
   -							placeCursorAtBeginning(input);
   -						}
   -					});
   -				}
   -			});
   -
   -			getThemesFromCSS();
   -			setInitialTheme();
   -			renderCurrentView();
   -			selectFirstSubtask();
   -		});
26 +		if ('serviceWorker' in navigator) {
27 +			navigator.serviceWorker.register('sw.js');
28 +		}
29  	</script>
30  </head>
31  <body>
32  	<div id="app-container"></div>
33  </body>
   -</html>
34 +</html>
readme.md
11  - unlimited subtask depth
12  - intuitive keyboard controls
13  - automatic saving using browser's local storage
   -- portable, "single html file" architecture that works completely offline
14 +- installable as a <a href="https://hunterirving.github.io/web_workshop/pages/pwa">Progressive Web App</a>
15  
16  ### quickstart
17  1. press the `Return` / `Enter` key to add subtasks to the root "todo" task
resources/keyboard.js
  1 +// Keyboard: key handler state and handleKeyDown dispatch
  2 +
  3 +var keyHandler = {
  4 +	backspace: {
  5 +		canDelete: true,
  6 +		blocked: false
  7 +	},
  8 +	enter: {
  9 +		canAdd: true,
 10 +		blocked: false
 11 +	},
 12 +	arrowDown: {
 13 +		canAdd: true,
 14 +		blocked: false
 15 +	},
 16 +	shiftArrowDown: {
 17 +		blocked: false
 18 +	},
 19 +	shiftEnter: {
 20 +		pressed: false
 21 +	},
 22 +	shiftRight: {
 23 +		pressed: false
 24 +	},
 25 +	shiftLeft: {
 26 +		pressed: false
 27 +	}
 28 +};
 29 +
 30 +function handleKeyDown(e, task) {
 31 +	var cmd = e.metaKey || e.ctrlKey;
 32 +	var hasMultiSelect = state.multiSelectedIds.length > 1;
 33 +
 34 +	// Alt+Up/Down: extend multi-selection
 35 +	if (e.key === 'ArrowUp' && e.altKey && !cmd && !e.shiftKey) {
 36 +		e.preventDefault();
 37 +		extendMultiSelect(task, 'up');
 38 +		return;
 39 +	} else if (e.key === 'ArrowDown' && e.altKey && !cmd && !e.shiftKey) {
 40 +		e.preventDefault();
 41 +		extendMultiSelect(task, 'down');
 42 +		return;
 43 +	}
 44 +
 45 +	// Shift+Enter: toggle task state (bulk if multi-selected)
 46 +	if (e.key === 'Enter' && e.shiftKey) {
 47 +		e.preventDefault();
 48 +		if (!keyHandler.shiftEnter.pressed) {
 49 +			keyHandler.shiftEnter.pressed = true;
 50 +			if (hasMultiSelect) {
 51 +				bulkToggleTaskState();
 52 +			} else {
 53 +				toggleTaskState(task);
 54 +			}
 55 +		}
 56 +	// Cmd+Up: push into task above
 57 +	} else if (e.key === 'ArrowUp' && cmd && !e.shiftKey && !e.altKey) {
 58 +		e.preventDefault();
 59 +		if (!e.repeat) {
 60 +			var success = hasMultiSelect ? pushMultiSelectedIntoTarget('up') : pushSubtaskIntoTarget(task, 'up');
 61 +			if (!success) {
 62 +				hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical');
 63 +			}
 64 +		}
 65 +	// Cmd+Down: push into task below
 66 +	} else if (e.key === 'ArrowDown' && cmd && !e.shiftKey && !e.altKey) {
 67 +		e.preventDefault();
 68 +		if (!e.repeat) {
 69 +			var success = hasMultiSelect ? pushMultiSelectedIntoTarget('down') : pushSubtaskIntoTarget(task, 'down');
 70 +			if (!success) {
 71 +				hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical');
 72 +			}
 73 +		}
 74 +	} else if (e.key === 'ArrowRight' && cmd && !e.shiftKey && !e.altKey) {
 75 +		e.preventDefault();
 76 +		hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
 77 +	// Cmd+Left: pull out one level
 78 +	} else if (e.key === 'ArrowLeft' && cmd && !e.shiftKey && !e.altKey) {
 79 +		e.preventDefault();
 80 +		if (!e.repeat) {
 81 +			var success = hasMultiSelect ? pullMultiSelectedOutLayer() : pullSubtaskOutLayer(task);
 82 +			if (!success) {
 83 +				hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
 84 +			}
 85 +		}
 86 +	// Cmd+Shift+Up: push and navigate
 87 +	} else if (e.key === 'ArrowUp' && cmd && e.shiftKey && !e.altKey) {
 88 +		e.preventDefault();
 89 +		if (!e.repeat) {
 90 +			var success = hasMultiSelect ? pushMultiSelectedIntoTarget('up', true) : pushSubtaskIntoTarget(task, 'up', true);
 91 +			if (!success) {
 92 +				hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical');
 93 +			}
 94 +		}
 95 +	// Cmd+Shift+Down: push and navigate
 96 +	} else if (e.key === 'ArrowDown' && cmd && e.shiftKey && !e.altKey) {
 97 +		e.preventDefault();
 98 +		if (!e.repeat) {
 99 +			var success = hasMultiSelect ? pushMultiSelectedIntoTarget('down', true) : pushSubtaskIntoTarget(task, 'down', true);
100 +			if (!success) {
101 +				hasMultiSelect ? shakeAllSelected('vertical') : applyShakeAnimation(task.id, 'vertical');
102 +			}
103 +		}
104 +	// Cmd+Shift+Left: pull and navigate
105 +	} else if (e.key === 'ArrowLeft' && cmd && e.shiftKey && !e.altKey) {
106 +		e.preventDefault();
107 +		if (!e.repeat) {
108 +			var success = hasMultiSelect ? pullMultiSelectedOutLayer(true) : pullSubtaskOutLayer(task, true);
109 +			if (!success) {
110 +				hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
111 +			}
112 +		}
113 +	} else if (e.key === 'ArrowRight' && cmd && e.shiftKey && !e.altKey) {
114 +		e.preventDefault();
115 +		hasMultiSelect ? shakeAllSelected() : applyShakeAnimation(task.id);
116 +	// Plain Up/Down: navigate (clears multi-select)
117 +	} else if (e.key === 'ArrowUp' && !e.shiftKey && !e.altKey) {
118 +		e.preventDefault();
119 +		if (hasMultiSelect) clearMultiSelect();
120 +		navigateTasks('up');
121 +	} else if (e.key === 'ArrowDown' && !e.shiftKey && !e.altKey) {
122 +		e.preventDefault();
123 +		if (hasMultiSelect) clearMultiSelect();
124 +		navigateTasks('down');
125 +	// Shift+Up/Down: move task
126 +	} else if (e.key === 'ArrowUp' && e.shiftKey && !e.altKey) {
127 +		e.preventDefault();
128 +		if (hasMultiSelect) {
129 +			if (!moveMultiSelected('up')) shakeAllSelected('vertical');
130 +		} else {
131 +			if (!moveSubtask(task, 'up')) applyShakeAnimation(task.id, 'vertical');
132 +		}
133 +	} else if (e.key === 'ArrowDown' && e.shiftKey && !e.altKey) {
134 +		e.preventDefault();
135 +		if (hasMultiSelect) {
136 +			if (!moveMultiSelected('down')) shakeAllSelected('vertical');
137 +		} else {
138 +			if (!moveSubtask(task, 'down')) applyShakeAnimation(task.id, 'vertical');
139 +		}
140 +	// Shift+Right: navigate into subtask (blocked during multi-select)
141 +	} else if (e.key === 'ArrowRight' && e.shiftKey && !e.altKey) {
142 +		e.preventDefault();
143 +		if (hasMultiSelect) {
144 +			if (!keyHandler.shiftRight.pressed) {
145 +				keyHandler.shiftRight.pressed = true;
146 +				shakeAllSelected();
147 +			}
148 +		} else if (!keyHandler.shiftRight.pressed) {
149 +			keyHandler.shiftRight.pressed = true;
150 +			if (task !== state.currentTask) {
151 +				navigateIntoSubtask(task);
152 +			} else {
153 +				applyShakeAnimation(task.id);
154 +			}
155 +		}
156 +	// Shift+Left: navigate to parent
157 +	} else if (e.key === 'ArrowLeft' && e.shiftKey && !e.altKey) {
158 +		e.preventDefault();
159 +		if (hasMultiSelect && state.currentTask.id === 'root') {
160 +			if (!keyHandler.shiftLeft.pressed) {
161 +				keyHandler.shiftLeft.pressed = true;
162 +				shakeAllSelected();
163 +			}
164 +		} else if (hasMultiSelect) {
165 +			clearMultiSelect();
166 +			if (!keyHandler.shiftLeft.pressed) {
167 +				keyHandler.shiftLeft.pressed = true;
168 +				navigateToParentTask();
169 +			}
170 +		} else if (!keyHandler.shiftLeft.pressed) {
171 +			keyHandler.shiftLeft.pressed = true;
172 +			navigateToParentTask();
173 +		}
174 +	}
175 +}
resources/main.css
  1 +/* custom colors */
  2 +:root {
  3 +	--coconut: #ffffff;
  4 +	--licorice: #000000;
  5 +	--wasabi: #ccff00;
  6 +	--eggplant: #7700ff;
  7 +
  8 +	--murk: #8e9918;
  9 +	--tadpole: #27350f;
 10 +	--lilypad: #6f8823;
 11 +	--reed: #4a5d23;
 12 +
 13 +	--pollen: #f4a127;
 14 +	--loam: #5a352b;
 15 +	--chlorophyll: #5aa83b;
 16 +	--terracotta: #b15c2e;
 17 +
 18 +	--wheat: #d2c3a3;
 19 +	--earth: #4a3c31;
 20 +	--pumpkin: #cb7c52;
 21 +	--tobacco: #7d6c55;
 22 +
 23 +	--moss: #20302f;
 24 +	--goat-milk: #d8d3c9;
 25 +	--burl: #231f20;
 26 +	--flame: #c63728;
 27 +
 28 +	--soot: #181a1b;
 29 +	--smoke: #e8e6e3;
 30 +	--ash: #252829;
 31 +	--cinder: #ff5532;
 32 +
 33 +	--blue-raspberry: #0458b3;
 34 +	--cola: #1e1515;
 35 +	--bubblegum: #e11761;
 36 +	--taffy: #eaeaea;
 37 +
 38 +	--muscat: #663399;
 39 +	--cask: #722f37;
 40 +	--bloom: #f8f6ff;
 41 +}
 42 +
 43 +/* custom fonts */
 44 +@font-face {
 45 +	font-family: BasteleurMoonlight;
 46 +	src: url('fonts/Basteleur-Moonlight.ttf') format('truetype');
 47 +}
 48 +
 49 +/* themes */
 50 +:root[data-theme="gak"] {
 51 +	--background: var(--coconut);
 52 +	--text: var(--licorice);
 53 +	--highlight: var(--wasabi);
 54 +	--accent: var(--eggplant);
 55 +	--icon: '🫟';
 56 +}
 57 +
 58 +:root[data-theme="swamp"] {
 59 +	--background: var(--murk);
 60 +	--text: var(--tadpole);
 61 +	--highlight: var(--lilypad);
 62 +	--accent: var(--reed);
 63 +	--icon: '🦠';
 64 +}
 65 +
 66 +:root[data-theme="sunflower"] {
 67 +	--background: var(--pollen);
 68 +	--text: var(--loam);
 69 +	--highlight: var(--chlorophyll);
 70 +	--accent: var(--terracotta);
 71 +	--icon: '🌻';
 72 +}
 73 +
 74 +:root[data-theme="harvest"] {
 75 +	--background: var(--wheat);
 76 +	--text: var(--earth);
 77 +	--highlight: var(--pumpkin);
 78 +	--accent: var(--tobacco);
 79 +	--icon: '🌾';
 80 +}
 81 +
 82 +:root[data-theme="medieval"] {
 83 +	--background: var(--moss);
 84 +	--text: var(--goat-milk);
 85 +	--highlight: var(--burl);
 86 +	--accent: var(--flame);
 87 +	--icon: '⚔️';
 88 +	& input[type="text"] {
 89 +		font-family: BasteleurMoonlight, Arial, sans-serif;
 90 +	}
 91 +}
 92 +
 93 +:root[data-theme="woodstove"] {
 94 +	--background: var(--soot);
 95 +	--text: var(--smoke);
 96 +	--highlight: var(--ash);
 97 +	--accent: var(--cinder);
 98 +	--icon: '🪵';
 99 +	& input[type="text"] {
100 +		font-family: BasteleurMoonlight, Arial, sans-serif;
101 +	}
102 +	.parent-task {
103 +		--text: var(--cinder)
104 +	}
105 +}
106 +
107 +:root[data-theme="sugar"] {
108 +	--background: var(--cola);
109 +	--text: var(--taffy);
110 +	--highlight: var(--blue-raspberry);
111 +	--accent: var(--bubblegum);
112 +	--icon: '🍬';
113 +
114 +	.checkbox-label {
115 +		border-radius: 10px;
116 +	}
117 +	.active {
118 +		border-radius: 30px 0px 0px 30px;
119 +	}
120 +	.custom-checkbox:checked + .checkbox-label::before {
121 +		display: none;
122 +	}
123 +	& input[type="text"] {
124 +		text-shadow: 1px 1px 1px var(--cola);
125 +	}
126 +	input[type="text"]::selection {
127 +		color: var(--text);
128 +	}
129 +}
130 +
131 +:root[data-theme="vineyard"] {
132 +	--background: var(--bloom);
133 +	--text: var(--cask);
134 +	--highlight: var(--muscat);
135 +	--accent: var(--muscat);
136 +	--icon: '🍇';
137 +	& input[type="text"] {
138 +		font-family: BasteleurMoonlight;
139 +	}
140 +	input[type="text"]::selection {
141 +		background-color: var(--background);
142 +		color: var(--text);
143 +		text-shadow: none;
144 +	}
145 +	.active {
146 +		border-radius: 30px 0px 0px 30px;
147 +	}
148 +	.active input[type="text"] {
149 +		color: var(--bloom);
150 +		text-shadow: 1px 1px 2px black;
151 +	}
152 +	.checkbox-label {
153 +		border-radius: 50%;
154 +		border: none;
155 +		background: radial-gradient(circle at 30% 30%, #d4a5d8, #b19cd9, #8a4baf);
156 +		box-shadow: 
157 +			inset 1px 1px 3px rgba(0,0,0,0.4),
158 +			inset -1px -1px 2px rgba(255,255,255,0.3),
159 +			0 1px 2px rgba(0,0,0,0.3);
160 +	}
161 +	.custom-checkbox:checked + .checkbox-label {
162 +		background: radial-gradient(circle at 30% 30%, #b19cd9, #8a4baf, #663399);
163 +		box-shadow: 
164 +			inset -2px -2px 4px rgba(0,0,0,0.3),
165 +			inset 1px 1px 2px rgba(255,255,255,0.5),
166 +			1px 1px 2px rgba(0,0,0,0.6);
167 +	}
168 +	.active .custom-checkbox:checked + .checkbox-label {
169 +		box-shadow: 
170 +			inset -2px -2px 4px rgba(0,0,0,0.3),
171 +			inset 1px 1px 2px rgba(255,255,255,0.5),
172 +			3px 3px 6px rgba(0,0,0,0.6);
173 +	}
174 +	.custom-checkbox:checked + .checkbox-label::before {
175 +		display: none;
176 +	}
177 +	.custom-checkbox:indeterminate + .checkbox-label::before {
178 +		content: '';
179 +		position: absolute;
180 +		left: 50%;
181 +		top: 50%;
182 +		width: 50%;
183 +		height: 50%;
184 +		border-radius: 50%;
185 +		background: radial-gradient(circle at 30% 30%, #b19cd9, #8a4baf, #663399);
186 +		box-shadow: 
187 +			inset -1px -1px 2px rgba(0,0,0,0.3),
188 +			inset 0.5px 0.5px 1px rgba(255,255,255,0.5);
189 +		transform: translate(-50%, -50%);
190 +	}
191 +	scrollbar-color: color-mix(in srgb, var(--muscat) 75%, transparent) color-mix(in srgb, var(--bloom) 75%, transparent);
192 +}
193 +
194 +/* interface styling */
195 +@keyframes shake {
196 +	0% { transform: translateX(0); }
197 +	20% { transform: translateX(3px); }
198 +	40% { transform: translateX(-3px); }
199 +	60% { transform: translateX(3px); }
200 +	80% { transform: translateX(-3px); }
201 +	100% { transform: translateX(0); }
202 +}
203 +.shake {
204 +	animation: shake 0.25s ease-out;
205 +}
206 +@keyframes shake-vertical {
207 +	0% { transform: translateY(0); }
208 +	20% { transform: translateY(3px); }
209 +	40% { transform: translateY(-3px); }
210 +	60% { transform: translateY(3px); }
211 +	80% { transform: translateY(-3px); }
212 +	100% { transform: translateY(0); }
213 +}
214 +.shake-vertical {
215 +	animation: shake-vertical 0.25s ease-out;
216 +}
217 +html {
218 +	scroll-behavior: smooth;
219 +	overflow-y: scroll;
220 +	overflow-x: hidden;
221 +	scrollbar-width: auto;
222 +	scrollbar-color: color-mix(in srgb, var(--text) 75%, transparent) color-mix(in srgb, var(--background) 75%, transparent);
223 +}
224 +body {
225 +	font-family: Arial, sans-serif;
226 +	max-width: 800px;
227 +	margin: 0 auto;
228 +	padding: 0px 20px 20px 20px;
229 +	background-color: var(--background);
230 +	color: var(--text);
231 +}
232 +ul {
233 +	list-style-type: none;
234 +	padding-left: 20px;
235 +}
236 +input[type="text"] {
237 +	border: none;
238 +	background: transparent;
239 +	font-size: 1.17em;
240 +	font-weight: bold;
241 +	width: calc(100% - 30px);
242 +	margin-left: 5px;
243 +	padding: 5px;
244 +	color: var(--text);
245 +	overflow: hidden;
246 +	text-overflow: ellipsis;
247 +	white-space: nowrap;
248 +}
249 +input[type="text"]:focus {
250 +	outline: none;
251 +}
252 +input[type="text"]::selection {
253 +	background-color: var(--accent);
254 +	color: var(--background);
255 +}
256 +.task-container {
257 +	display: flex;
258 +	align-items: center;
259 +	margin: 5px 0;
260 +	padding-left: 11px;
261 +	padding-top: 4px;
262 +	padding-bottom: 4px;
263 +}
264 +.active {
265 +	background-color: var(--highlight);
266 +}
267 +.sticky-header {
268 +	position: sticky;
269 +	top: 0;
270 +	background-color: var(--background);
271 +	z-index: 1;
272 +	padding-top: 20px;
273 +}
274 +.parent-task {
275 +	font-size: 1.5em;
276 +	font-weight: bold;
277 +}
278 +#breadcrumbs {
279 +	font-size: 24px;
280 +	margin-bottom: 10px;
281 +	color: var(--text);
282 +	user-select: none;
283 +}
284 +.custom-checkbox {
285 +	display: none;
286 +}
287 +.checkbox-label {
288 +	display: inline-block;
289 +	width: 20px;
290 +	height: 20px;
291 +	background-color: var(--background);
292 +	border: 2px solid var(--accent);
293 +	position: relative;
294 +	cursor: pointer;
295 +	box-sizing: border-box;
296 +	border-radius: 4px;
297 +}
298 +.custom-checkbox:checked + .checkbox-label {
299 +	background-color: var(--accent);
300 +}
301 +.custom-checkbox:checked + .checkbox-label::before {
302 +	content: '';
303 +	position: absolute;
304 +	left: 6px;
305 +	top: 2px;
306 +	width: 5px;
307 +	height: 10px;
308 +	border: solid var(--background);
309 +	border-width: 0 2px 2px 0;
310 +	transform: rotate(45deg);
311 +	box-sizing: border-box;
312 +}
313 +.custom-checkbox:indeterminate + .checkbox-label::before {
314 +	content: '';
315 +	position: absolute;
316 +	left: 25%;
317 +	right: 25%;
318 +	top: 50%;
319 +	height: 2px;
320 +	background-color: var(--accent);
321 +	transform: translateY(-50%);
322 +}
323 +
324 +/* print styles */
325 +@media print {
326 +	body * {
327 +		visibility: hidden;
328 +	}
329 +	#print-content, #print-content * {
330 +		visibility: visible;
331 +	}
332 +	#print-content {
333 +		position: absolute;
334 +		left: 0;
335 +		top: 0;
336 +		font-family: monospace;
337 +		font-size: 12pt;
338 +		line-height: 1.4;
339 +		white-space: pre-wrap;
340 +		color: black;
341 +		background: white;
342 +	}
343 +	#app-container {
344 +		display: none;
345 +	}
346 +}
resources/main.js
 1 +// Main: initialization and global event listeners
 2 +
 3 +document.addEventListener('click', function(e) {
 4 +	if (!e.target.closest('.task-container')) {
 5 +		var activeTask = document.querySelector('.task-container.active');
 6 +		if (activeTask) {
 7 +			var taskInput = activeTask.querySelector('input[type="text"]');
 8 +			taskInput.focus();
 9 +		} else {
10 +			selectFirstSubtask();
11 +		}
12 +	}
13 +});
14 +
15 +document.addEventListener('DOMContentLoaded', function() {
16 +	state.appContainer = document.getElementById('app-container');
17 +	state.rootTask = loadTasksFromLocalStorage();
18 +	state.currentTask = state.rootTask;
19 +	state.taskPath = [state.currentTask];
20 +
21 +	// Scroll handler to block default scroll behavior
22 +	function handleScroll(e) {
23 +		e.preventDefault();
24 +	}
25 +	window.addEventListener('wheel', handleScroll, { passive: false });
26 +
27 +	// Print handlers
28 +	function handleBeforePrint() {
29 +		var printContent = document.getElementById('print-content');
30 +		if (!printContent) {
31 +			printContent = document.createElement('div');
32 +			printContent.id = 'print-content';
33 +			document.body.appendChild(printContent);
34 +		}
35 +		var serializedTasks = serializeTaskTree(state.currentTask);
36 +		printContent.textContent = serializedTasks;
37 +	}
38 +
39 +	function handleAfterPrint() {
40 +		var printContent = document.getElementById('print-content');
41 +		if (printContent) {
42 +			printContent.remove();
43 +		}
44 +	}
45 +
46 +	window.addEventListener('beforeprint', handleBeforePrint);
47 +	window.addEventListener('afterprint', handleAfterPrint);
48 +
49 +	// Theme cycling with F2
50 +	document.addEventListener('keydown', function(event) {
51 +		if (event.key === 'F2' && !state.isF2Pressed) {
52 +			event.preventDefault();
53 +			state.isF2Pressed = true;
54 +			cycleTheme();
55 +		}
56 +	});
57 +
58 +	document.addEventListener('keyup', function(event) {
59 +		if (event.key === 'F2') {
60 +			state.isF2Pressed = false;
61 +		}
62 +	});
63 +
64 +	// File save/open handlers
65 +	document.addEventListener('keydown', handleSave);
66 +	document.addEventListener('keydown', handleOpen);
67 +
68 +	// Reset cursor on non-focused inputs
69 +	state.appContainer.addEventListener('focusin', function(e) {
70 +		if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
71 +			document.querySelectorAll('input[type="text"]').forEach(input => {
72 +				if (input !== e.target) {
73 +					placeCursorAtBeginning(input);
74 +				}
75 +			});
76 +		}
77 +	});
78 +
79 +	// Initialize
80 +	getThemesFromCSS();
81 +	setInitialTheme();
82 +	renderCurrentView();
83 +	selectFirstSubtask();
84 +});
resources/multi-select.js
  1 +// Multi-select: state management and bulk operations
  2 +
  3 +function clearMultiSelect() {
  4 +	state.multiSelectAnchorId = null;
  5 +	state.multiSelectedIds = [];
  6 +	var focused = document.activeElement;
  7 +	var focusedContainer = focused ? focused.closest('.task-container') : null;
  8 +	document.querySelectorAll('.task-container.active').forEach(el => {
  9 +		if (el !== focusedContainer) el.classList.remove('active');
 10 +	});
 11 +}
 12 +
 13 +function getMultiSelectedTasks() {
 14 +	return state.currentTask.subtasks.filter(t => state.multiSelectedIds.includes(t.id));
 15 +}
 16 +
 17 +function applyMultiSelectHighlights() {
 18 +	for (var id of state.multiSelectedIds) {
 19 +		var container = document.querySelector(`.task-container[data-id="${id}"]`);
 20 +		if (container) container.classList.add('active');
 21 +	}
 22 +}
 23 +
 24 +function shakeAllSelected(direction = 'horizontal') {
 25 +	for (var id of state.multiSelectedIds) {
 26 +		applyShakeAnimation(id, direction);
 27 +	}
 28 +}
 29 +
 30 +function extendMultiSelect(task, direction) {
 31 +	if (task === state.currentTask) return;
 32 +
 33 +	var tasks = state.currentTask.subtasks;
 34 +	if (tasks.length === 0) return;
 35 +
 36 +	if (state.multiSelectAnchorId === null) {
 37 +		state.multiSelectAnchorId = task.id;
 38 +		state.multiSelectedIds = [task.id];
 39 +	}
 40 +
 41 +	var anchorIndex = tasks.findIndex(t => t.id === state.multiSelectAnchorId);
 42 +	var currentIndex = tasks.findIndex(t => t.id === task.id);
 43 +
 44 +	if (direction === 'down') {
 45 +		var bottomIndex = tasks.findIndex((t, i) => {
 46 +			return state.multiSelectedIds.includes(t.id) &&
 47 +				(i === tasks.length - 1 || !state.multiSelectedIds.includes(tasks[i + 1].id));
 48 +		});
 49 +		if (currentIndex <= anchorIndex && state.multiSelectedIds.length > 1) {
 50 +			state.multiSelectedIds = state.multiSelectedIds.filter(id => id !== task.id);
 51 +			var nextTask = tasks[currentIndex + 1];
 52 +			if (nextTask) selectAndFocusTask(nextTask);
 53 +		} else if (bottomIndex < tasks.length - 1) {
 54 +			var nextTask = tasks[bottomIndex + 1];
 55 +			state.multiSelectedIds.push(nextTask.id);
 56 +			selectAndFocusTask(nextTask);
 57 +		}
 58 +	} else {
 59 +		var topIndex = tasks.findIndex(t => state.multiSelectedIds.includes(t.id));
 60 +		if (currentIndex >= anchorIndex && state.multiSelectedIds.length > 1) {
 61 +			state.multiSelectedIds = state.multiSelectedIds.filter(id => id !== task.id);
 62 +			var prevTask = tasks[currentIndex - 1];
 63 +			if (prevTask) selectAndFocusTask(prevTask);
 64 +		} else if (topIndex > 0) {
 65 +			var prevTask = tasks[topIndex - 1];
 66 +			state.multiSelectedIds.unshift(prevTask.id);
 67 +			selectAndFocusTask(prevTask);
 68 +		}
 69 +	}
 70 +
 71 +	applyMultiSelectHighlights();
 72 +}
 73 +
 74 +function moveMultiSelected(direction) {
 75 +	var selected = getMultiSelectedTasks();
 76 +	if (selected.length === 0) return false;
 77 +
 78 +	var subtasks = state.currentTask.subtasks;
 79 +	var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b);
 80 +	var topIndex = indices[0];
 81 +	var bottomIndex = indices[indices.length - 1];
 82 +
 83 +	if (direction === 'up' && topIndex <= 0) return false;
 84 +	if (direction === 'down' && bottomIndex >= subtasks.length - 1) return false;
 85 +
 86 +	var chunk = subtasks.splice(topIndex, selected.length);
 87 +	var insertAt = direction === 'up' ? topIndex - 1 : topIndex + 1;
 88 +	subtasks.splice(insertAt, 0, ...chunk);
 89 +
 90 +	state.currentTask.selectedSubtaskId = state.multiSelectAnchorId;
 91 +	renderCurrentView();
 92 +	applyMultiSelectHighlights();
 93 +	scheduleSave();
 94 +	return true;
 95 +}
 96 +
 97 +function pushMultiSelectedIntoTarget(direction, navigate = false) {
 98 +	var selected = getMultiSelectedTasks();
 99 +	if (selected.length === 0) return false;
100 +
101 +	if (navigate) {
102 +		document.documentElement.style.scrollBehavior = 'auto';
103 +	}
104 +
105 +	var subtasks = state.currentTask.subtasks;
106 +	var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b);
107 +	var topIndex = indices[0];
108 +	var bottomIndex = indices[indices.length - 1];
109 +
110 +	var targetTask = null;
111 +	if (direction === 'up' && topIndex > 0) {
112 +		targetTask = subtasks[topIndex - 1];
113 +	} else if (direction === 'down' && bottomIndex < subtasks.length - 1) {
114 +		targetTask = subtasks[bottomIndex + 1];
115 +	}
116 +	if (!targetTask) return false;
117 +
118 +	var chunk = selected.slice();
119 +	state.currentTask.subtasks = state.currentTask.subtasks.filter(t => !state.multiSelectedIds.includes(t.id));
120 +
121 +	targetTask.subtasks.unshift(...chunk);
122 +
123 +	for (var t of chunk) {
124 +		adjustMovedTaskState(t, targetTask);
125 +	}
126 +	targetTask.selectedSubtaskId = state.multiSelectAnchorId;
127 +	updateTaskAndAncestors(state.currentTask);
128 +	updateTaskAndAncestors(targetTask);
129 +
130 +	if (navigate) {
131 +		state.currentTask.selectedSubtaskId = targetTask.id;
132 +		state.taskPath.push(targetTask);
133 +		state.currentTask = targetTask;
134 +		updateBreadcrumbs(state.currentTask);
135 +		renderCurrentView();
136 +		applyMultiSelectHighlights();
137 +	} else {
138 +		clearMultiSelect();
139 +		renderCurrentView();
140 +		selectAndFocusTask(targetTask);
141 +	}
142 +
143 +	scheduleSave();
144 +	return true;
145 +}
146 +
147 +function pullMultiSelectedOutLayer(navigate = false) {
148 +	if (state.taskPath.length <= 1) return false;
149 +
150 +	var selected = getMultiSelectedTasks();
151 +	if (selected.length === 0) return false;
152 +
153 +	if (navigate) {
154 +		document.documentElement.style.scrollBehavior = 'auto';
155 +	}
156 +
157 +	var currentParent = state.currentTask;
158 +	var grandParent = state.taskPath[state.taskPath.length - 2];
159 +	if (!grandParent) return false;
160 +
161 +	currentParent.subtasks = currentParent.subtasks.filter(t => !state.multiSelectedIds.includes(t.id));
162 +
163 +	var parentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id);
164 +	if (parentIndex === -1) {
165 +		grandParent.subtasks.push(...selected);
166 +	} else {
167 +		grandParent.subtasks.splice(parentIndex + 1, 0, ...selected);
168 +	}
169 +
170 +	for (var t of selected) {
171 +		adjustMovedTaskState(t, grandParent);
172 +	}
173 +
174 +	updateTaskAndAncestors(currentParent);
175 +	updateTaskAndAncestors(grandParent);
176 +
177 +	if (navigate || currentParent.subtasks.length === 0) {
178 +		state.taskPath.pop();
179 +		state.currentTask = state.taskPath[state.taskPath.length - 1];
180 +		state.currentTask.selectedSubtaskId = state.multiSelectAnchorId;
181 +		updateBreadcrumbs(state.currentTask);
182 +		renderCurrentView();
183 +		applyMultiSelectHighlights();
184 +	} else {
185 +		clearMultiSelect();
186 +		renderCurrentView();
187 +		if (currentParent.subtasks.length > 0) {
188 +			selectAndFocusTask(currentParent.subtasks[0]);
189 +		} else {
190 +			selectAndFocusTask(currentParent);
191 +		}
192 +	}
193 +
194 +	scheduleSave();
195 +	return true;
196 +}
197 +
198 +function bulkToggleTaskState() {
199 +	var selected = getMultiSelectedTasks();
200 +	if (selected.length === 0) return;
201 +
202 +	var anyUnchecked = selected.some(t => t.state !== 1);
203 +	var newState = anyUnchecked ? 1 : 0;
204 +
205 +	for (var t of selected) {
206 +		function setAllSubtasks(task, s) {
207 +			task.state = s;
208 +			for (var sub of task.subtasks) {
209 +				setAllSubtasks(sub, s);
210 +			}
211 +		}
212 +		setAllSubtasks(t, newState);
213 +	}
214 +
215 +	updateTaskAndAncestors(state.currentTask);
216 +	state.currentTask.selectedSubtaskId = state.multiSelectAnchorId;
217 +	renderCurrentView();
218 +	applyMultiSelectHighlights();
219 +	scheduleSave();
220 +}
resources/navigation.js
  1 +// Navigation: moving between tasks, navigating into/out of subtask levels
  2 +
  3 +function navigateTasks(direction) {
  4 +	var tasks = state.currentTask.subtasks;
  5 +	var subtaskIndex = tasks.findIndex(t => t.id === state.currentTask.selectedSubtaskId);
  6 +
  7 +	var currentElement = document.activeElement;
  8 +	var currentContainer = currentElement ? currentElement.closest('.task-container') : null;
  9 +	var isParentFocused = currentContainer && currentContainer.dataset.id == state.currentTask.id;
 10 +
 11 +	if (direction === 'up') {
 12 +		if (isParentFocused) {
 13 +			// Already at parent, can't go higher
 14 +		} else if (subtaskIndex <= 0) {
 15 +			selectAndFocusTask(state.currentTask);
 16 +		} else {
 17 +			selectAndFocusTask(tasks[subtaskIndex - 1]);
 18 +		}
 19 +	} else {
 20 +		if (isParentFocused) {
 21 +			if (tasks.length > 0) {
 22 +				selectAndFocusTask(tasks[0]);
 23 +			}
 24 +		} else if (subtaskIndex >= 0 && subtaskIndex < tasks.length - 1) {
 25 +			selectAndFocusTask(tasks[subtaskIndex + 1]);
 26 +		}
 27 +	}
 28 +	state.lastSubtaskDownArrowReleased = false;
 29 +	state.lastSubtaskShiftDownReleased = false;
 30 +}
 31 +
 32 +function navigateIntoSubtask(subtask) {
 33 +	document.documentElement.style.scrollBehavior = 'auto';
 34 +
 35 +	if (subtask.subtasks.length > 0) {
 36 +		state.currentTask.selectedSubtaskId = subtask.id;
 37 +		state.taskPath.push(subtask);
 38 +		state.currentTask = subtask;
 39 +		updateBreadcrumbs(state.currentTask);
 40 +		renderCurrentView();
 41 +		var selectedSubtask = subtask.selectedSubtaskId ?
 42 +			subtask.subtasks.find(t => t.id === subtask.selectedSubtaskId) :
 43 +			subtask.subtasks[0];
 44 +		selectAndFocusTask(selectedSubtask);
 45 +	} else {
 46 +		addNewSubtask(subtask);
 47 +		state.currentTask.selectedSubtaskId = subtask.id;
 48 +		state.taskPath.push(subtask);
 49 +		state.currentTask = subtask;
 50 +		updateBreadcrumbs(state.currentTask);
 51 +		renderCurrentView();
 52 +		selectAndFocusTask(subtask.subtasks[0]);
 53 +	}
 54 +}
 55 +
 56 +function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) {
 57 +	document.documentElement.style.scrollBehavior = 'auto';
 58 +
 59 +	state.currentTask.selectedSubtaskId = targetTask.id;
 60 +
 61 +	state.taskPath.push(targetTask);
 62 +	state.currentTask = targetTask;
 63 +	updateBreadcrumbs(state.currentTask);
 64 +	renderCurrentView();
 65 +
 66 +	selectAndFocusTask(subtaskToSelect);
 67 +	state.currentTask.selectedSubtaskId = subtaskToSelect.id;
 68 +}
 69 +
 70 +function navigateToParentTask() {
 71 +	if (state.currentTask.id === 'root') {
 72 +		var activeTaskElement = document.querySelector('.task-container.active');
 73 +		if (activeTaskElement) {
 74 +			var taskId = activeTaskElement.dataset.id;
 75 +			applyShakeAnimation(taskId);
 76 +		}
 77 +	} else if (state.taskPath.length > 1) {
 78 +		document.documentElement.style.scrollBehavior = 'auto';
 79 +
 80 +		var currentTaskId = state.currentTask.id;
 81 +		state.taskPath.pop();
 82 +		state.currentTask = state.taskPath[state.taskPath.length - 1];
 83 +		updateBreadcrumbs(state.currentTask);
 84 +		renderCurrentView();
 85 +		var selectedSubtask = state.currentTask.subtasks.find(t => t.id === currentTaskId);
 86 +		if (selectedSubtask) {
 87 +			selectAndFocusTask(selectedSubtask);
 88 +		} else if (state.currentTask.subtasks.length > 0) {
 89 +			selectAndFocusTask(state.currentTask.subtasks[0]);
 90 +		} else {
 91 +			selectAndFocusTask(state.currentTask);
 92 +		}
 93 +		state.currentTask.selectedSubtaskId = currentTaskId;
 94 +	}
 95 +}
 96 +
 97 +function navigateToParentTaskAndSelectTask(targetTask) {
 98 +	if (state.currentTask.id === 'root') {
 99 +		renderCurrentView();
100 +		selectAndFocusTask(targetTask);
101 +	} else if (state.taskPath.length > 1) {
102 +		document.documentElement.style.scrollBehavior = 'auto';
103 +
104 +		state.taskPath.pop();
105 +		state.currentTask = state.taskPath[state.taskPath.length - 1];
106 +		updateBreadcrumbs(state.currentTask);
107 +		renderCurrentView();
108 +		selectAndFocusTask(targetTask);
109 +		state.currentTask.selectedSubtaskId = targetTask.id;
110 +	}
111 +}
resources/render.js
  1 +// Render: view rendering, breadcrumbs, UI updates, and shared DOM helpers
  2 +
  3 +function generateBreadcrumbs(rootTask, currentPath, selectedTaskId) {
  4 +	var breadcrumbs = '';
  5 +	var currentTask = rootTask;
  6 +	var currentDepth = 0;
  7 +
  8 +	for (var i = 0; i < currentPath.length - 1; i++) {
  9 +		breadcrumbs += '○ ';
 10 +		currentTask = currentTask.subtasks.find(t => t.id === currentPath[i + 1].id);
 11 +	}
 12 +
 13 +	breadcrumbs += '● ';
 14 +	currentDepth = currentPath.length - 1;
 15 +
 16 +	if (selectedTaskId !== currentTask.id) {
 17 +		var selectedTask = currentTask.subtasks.find(t => t.id === selectedTaskId);
 18 +
 19 +		if (selectedTask) {
 20 +			function calculateMaxDepth(task, depth) {
 21 +				if (task.subtasks.length === 0) return depth;
 22 +				return Math.max(...task.subtasks.map(st => calculateMaxDepth(st, depth + 1)));
 23 +			}
 24 +
 25 +			var maxDepth = calculateMaxDepth(selectedTask, currentDepth + 1);
 26 +
 27 +			for (var i = currentDepth + 1; i < maxDepth; i++) {
 28 +				breadcrumbs += '○ ';
 29 +			}
 30 +		}
 31 +	}
 32 +
 33 +	return breadcrumbs.trim();
 34 +}
 35 +
 36 +function applyShakeAnimation(taskId, direction = 'horizontal') {
 37 +	var checkbox = document.querySelector(`.task-container[data-id="${taskId}"] .checkbox-label`);
 38 +	if (checkbox) {
 39 +		var className = direction === 'vertical' ? 'shake-vertical' : 'shake';
 40 +		if (checkbox.dataset.shaking) return;
 41 +		checkbox.dataset.shaking = '1';
 42 +		checkbox.classList.add(className);
 43 +		checkbox.addEventListener('animationend', () => {
 44 +			checkbox.classList.remove(className);
 45 +		}, { once: true });
 46 +		var clearShaking = () => {
 47 +			delete checkbox.dataset.shaking;
 48 +			document.removeEventListener('keyup', clearShaking);
 49 +		};
 50 +		document.addEventListener('keyup', clearShaking);
 51 +	}
 52 +}
 53 +
 54 +function selectAndFocusTask(task, cursorPos) {
 55 +	var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
 56 +	if (taskInput) {
 57 +		taskInput.focus();
 58 +		var pos = cursorPos != null ? cursorPos : taskInput.value.length;
 59 +		taskInput.setSelectionRange(pos, pos);
 60 +		setActiveTask(taskInput, task);
 61 +	}
 62 +}
 63 +
 64 +function placeCursorAtBeginning(input) {
 65 +	input.setSelectionRange(0, 0);
 66 +}
 67 +
 68 +function setActiveTask(input, task) {
 69 +	document.querySelectorAll('.active').forEach(el => el.classList.remove('active'));
 70 +	input.closest('.task-container').classList.add('active');
 71 +	if (task !== state.currentTask) {
 72 +		state.currentTask.selectedSubtaskId = task.id;
 73 +	}
 74 +	// Re-apply multi-select highlights after clearing
 75 +	if (state.multiSelectedIds.length > 1) {
 76 +		applyMultiSelectHighlights();
 77 +	}
 78 +	updateBreadcrumbs(task);
 79 +	// During multi-select, check bottommost selected task for "last subtask" status
 80 +	var bottomTask = state.multiSelectedIds.length > 1
 81 +		? getMultiSelectedTasks().slice(-1)[0]
 82 +		: task;
 83 +	state.lastSubtaskDownArrowReleased = isLastSubtask(bottomTask);
 84 +	state.lastSubtaskShiftDownReleased = isLastSubtask(bottomTask);
 85 +	input.focus();
 86 +
 87 +	// Center the active task in the viewport
 88 +	var activeTaskElement = input.closest('.task-container');
 89 +	if (activeTaskElement && activeTaskElement.parentElement && activeTaskElement.parentElement.tagName === 'LI') {
 90 +		activeTaskElement.scrollIntoView({
 91 +			behavior: 'auto',
 92 +			block: 'center',
 93 +			inline: 'nearest'
 94 +		});
 95 +
 96 +		setTimeout(() => {
 97 +			document.documentElement.style.scrollBehavior = 'smooth';
 98 +		}, 200);
 99 +	}
100 +}
101 +
102 +function updateBreadcrumbs(selectedTask) {
103 +	var breadcrumbsContainer = document.getElementById('breadcrumbs');
104 +	var effectiveId = state.multiSelectedIds.length > 1 ? state.currentTask.id : selectedTask.id;
105 +	var trail = generateBreadcrumbs(state.taskPath[0], state.taskPath, effectiveId);
106 +	breadcrumbsContainer.textContent = trail;
107 +}
108 +
109 +function updatePageTitle(task) {
110 +	document.title = task.text || '?';
111 +}
112 +
113 +function selectFirstSubtask() {
114 +	if (state.currentTask.subtasks.length > 0) {
115 +		var firstSubtask = state.currentTask.subtasks[0];
116 +		selectAndFocusTask(firstSubtask);
117 +	} else {
118 +		selectAndFocusTask(state.currentTask);
119 +	}
120 +}
121 +
122 +function handleCopyAndCut(e) {
123 +	if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'x')) {
124 +		var activeTaskInput = document.querySelector('.task-container.active input[type="text"]');
125 +		if (activeTaskInput) {
126 +			e.preventDefault();
127 +
128 +			if (activeTaskInput.selectionStart === activeTaskInput.selectionEnd) {
129 +				activeTaskInput.select();
130 +			}
131 +
132 +			if (e.key === 'c') {
133 +				document.execCommand('copy');
134 +			} else if (e.key === 'x') {
135 +				document.execCommand('cut');
136 +
137 +				var taskContainer = activeTaskInput.closest('.task-container');
138 +				var taskId = taskContainer.dataset.id;
139 +				var task = state.currentTask.id === taskId ? state.currentTask : state.currentTask.subtasks.find(t => t.id === taskId);
140 +				if (task) {
141 +					task.text = activeTaskInput.value;
142 +					scheduleSave();
143 +				}
144 +			}
145 +
146 +			if (e.key === 'c' && activeTaskInput.selectionStart === 0 && activeTaskInput.selectionEnd === activeTaskInput.value.length) {
147 +				activeTaskInput.setSelectionRange(activeTaskInput.value.length, activeTaskInput.value.length);
148 +			}
149 +		}
150 +	}
151 +}
152 +
153 +function renderCurrentView() {
154 +	state.appContainer.innerHTML = '';
155 +	state.currentTask = state.taskPath[state.taskPath.length - 1];
156 +
157 +	var stickyHeader = document.createElement('div');
158 +	stickyHeader.className = 'sticky-header';
159 +
160 +	var breadcrumbsElement = document.createElement('div');
161 +	breadcrumbsElement.id = 'breadcrumbs';
162 +	stickyHeader.appendChild(breadcrumbsElement);
163 +
164 +	var parentElement = createTaskElement(state.currentTask, true);
165 +	stickyHeader.appendChild(parentElement);
166 +
167 +	state.appContainer.appendChild(stickyHeader);
168 +
169 +	var subtasksContainer = document.createElement('div');
170 +	subtasksContainer.id = 'subtasks-container';
171 +
172 +	var subtasksList = document.createElement('ul');
173 +	state.currentTask.subtasks.forEach(subtask => {
174 +		var li = document.createElement('li');
175 +		li.appendChild(createTaskElement(subtask));
176 +		subtasksList.appendChild(li);
177 +	});
178 +	subtasksContainer.appendChild(subtasksList);
179 +	state.appContainer.appendChild(subtasksContainer);
180 +
181 +	updateBreadcrumbs(state.currentTask);
182 +	updatePageTitle(state.currentTask);
183 +
184 +	var parentCheckbox = parentElement.querySelector('input[type="checkbox"]');
185 +	updateCheckboxState(parentCheckbox, state.currentTask.state);
186 +
187 +	if (state.currentTask.selectedSubtaskId) {
188 +		var selectedTask = state.currentTask.subtasks.find(t => t.id === state.currentTask.selectedSubtaskId);
189 +		if (selectedTask) {
190 +			selectAndFocusTask(selectedTask);
191 +		} else {
192 +			selectFirstSubtask();
193 +		}
194 +	} else {
195 +		selectFirstSubtask();
196 +	}
197 +}
resources/reorganize.js
  1 +// Reorganize: single-task move, push, and pull operations
  2 +
  3 +function moveSubtask(subtask, direction) {
  4 +	if (subtask === state.currentTask) return false;
  5 +
  6 +	var parentTask = findParentTask(subtask);
  7 +	if (!parentTask) return false;
  8 +
  9 +	var index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
 10 +	if (index === -1) return false;
 11 +
 12 +	if (direction === 'up' && index > 0) {
 13 +		[parentTask.subtasks[index - 1], parentTask.subtasks[index]] = [parentTask.subtasks[index], parentTask.subtasks[index - 1]];
 14 +	} else if (direction === 'down' && index < parentTask.subtasks.length - 1) {
 15 +		[parentTask.subtasks[index], parentTask.subtasks[index + 1]] = [parentTask.subtasks[index + 1], parentTask.subtasks[index]];
 16 +	} else {
 17 +		return false;
 18 +	}
 19 +
 20 +	renderCurrentView();
 21 +	selectAndFocusTask(subtask);
 22 +	scheduleSave();
 23 +	return true;
 24 +}
 25 +
 26 +function pushSubtaskIntoTarget(subtask, direction, navigate = false) {
 27 +	if (subtask === state.currentTask) return false;
 28 +
 29 +	if (navigate) {
 30 +		document.documentElement.style.scrollBehavior = 'auto';
 31 +	}
 32 +
 33 +	var parentTask = findParentTask(subtask);
 34 +	if (!parentTask) return false;
 35 +
 36 +	var index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
 37 +	if (index === -1) return false;
 38 +
 39 +	var targetTask = null;
 40 +	if (direction === 'up' && index > 0) {
 41 +		targetTask = parentTask.subtasks[index - 1];
 42 +	} else if (direction === 'down' && index < parentTask.subtasks.length - 1) {
 43 +		targetTask = parentTask.subtasks[index + 1];
 44 +	}
 45 +
 46 +	if (!targetTask) return false;
 47 +
 48 +	parentTask.subtasks.splice(index, 1);
 49 +	targetTask.subtasks.unshift(subtask);
 50 +	targetTask.selectedSubtaskId = subtask.id;
 51 +
 52 +	adjustMovedTaskState(subtask, targetTask);
 53 +	updateTaskAndAncestors(parentTask);
 54 +	updateTaskAndAncestors(targetTask);
 55 +
 56 +	if (navigate) {
 57 +		navigateIntoTaskAndSelectSubtask(targetTask, subtask);
 58 +	} else {
 59 +		renderCurrentView();
 60 +		selectAndFocusTask(targetTask);
 61 +	}
 62 +
 63 +	scheduleSave();
 64 +	return true;
 65 +}
 66 +
 67 +function pullSubtaskOutLayer(subtask, navigate = false) {
 68 +	if (subtask === state.currentTask) return false;
 69 +	if (state.taskPath.length <= 1) return false;
 70 +
 71 +	if (navigate) {
 72 +		document.documentElement.style.scrollBehavior = 'auto';
 73 +	}
 74 +
 75 +	var currentParent = findParentTask(subtask);
 76 +	if (!currentParent) return;
 77 +
 78 +	var grandParent = findParentTask(currentParent);
 79 +	if (!grandParent) return;
 80 +
 81 +	var currentIndex = currentParent.subtasks.findIndex(t => t.id === subtask.id);
 82 +	if (currentIndex === -1) return;
 83 +	currentParent.subtasks.splice(currentIndex, 1);
 84 +
 85 +	var currentParentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id);
 86 +	if (currentParentIndex === -1) {
 87 +		grandParent.subtasks.push(subtask);
 88 +	} else {
 89 +		grandParent.subtasks.splice(currentParentIndex + 1, 0, subtask);
 90 +	}
 91 +
 92 +	adjustMovedTaskState(subtask, grandParent);
 93 +	updateTaskAndAncestors(currentParent);
 94 +	updateTaskAndAncestors(grandParent);
 95 +
 96 +	if (navigate || (currentParent.subtasks.length === 0 && state.taskPath.length > 1)) {
 97 +		navigateToParentTaskAndSelectTask(subtask);
 98 +	} else {
 99 +		renderCurrentView();
100 +		if (currentParent.subtasks.length > 0) {
101 +			var targetIndex = Math.max(0, currentIndex - 1);
102 +			selectAndFocusTask(currentParent.subtasks[targetIndex]);
103 +		} else {
104 +			selectAndFocusTask(currentParent);
105 +		}
106 +	}
107 +
108 +	scheduleSave();
109 +	return true;
110 +}
resources/state.js
 1 +// Shared mutable application state
 2 +
 3 +var state = {
 4 +	appContainer: null,
 5 +	rootTask: null,
 6 +	currentTask: null,
 7 +	taskPath: [],
 8 +	lastSubtaskDownArrowReleased: false,
 9 +	lastSubtaskShiftDownReleased: false,
10 +	saveTimer: null,
11 +	currentThemeIndex: 0,
12 +	isF2Pressed: false,
13 +	themes: [],
14 +	isInWheelEvent: false,
15 +
16 +	// Multi-select state
17 +	multiSelectAnchorId: null,
18 +	multiSelectedIds: [],
19 +};
20 +
21 +function generateId() {
22 +	return Date.now().toString(36) + Math.random().toString(36).slice(2);
23 +}
resources/storage.js
  1 +// Storage: localStorage persistence, file import/export, serialization
  2 +
  3 +function scheduleSave() {
  4 +	if (state.saveTimer) {
  5 +		clearTimeout(state.saveTimer);
  6 +	}
  7 +	state.saveTimer = setTimeout(saveTasksToLocalStorage, 1000);
  8 +}
  9 +
 10 +function saveTasksToLocalStorage() {
 11 +	var serializedTasks = serializeTaskTree(state.taskPath[0]);
 12 +	localStorage.setItem('taskTree', serializedTasks);
 13 +	state.saveTimer = null;
 14 +}
 15 +
 16 +function serializeTaskTree(task, depth = 0) {
 17 +	var indentation = '\t'.repeat(depth);
 18 +	var status = task.state === 0 ? '_' : (task.state === 1 ? 'x' : '?');
 19 +	var serialized = `${indentation}${status} ${task.text}\n`;
 20 +
 21 +	for (var subtask of task.subtasks) {
 22 +		serialized += serializeTaskTree(subtask, depth + 1);
 23 +	}
 24 +
 25 +	return serialized;
 26 +}
 27 +
 28 +function deserializeTaskTree(serialized) {
 29 +	var lines = serialized.split('\n').filter(line => line.trim() !== '');
 30 +	var root = { id: 'root', subtasks: [] };
 31 +	var stack = [{ task: root, depth: -1 }];
 32 +
 33 +	for (var line of lines) {
 34 +		var depth = (line.match(/^\t*/)[0] || '').length;
 35 +		var status = line[depth];
 36 +		var text = line.slice(depth + 2);
 37 +
 38 +		var newTask = {
 39 +			id: depth === 0 ? 'root' : generateId(),
 40 +			text: text,
 41 +			state: status === '_' ? 0 : (status === 'x' ? 1 : 2),
 42 +			subtasks: [],
 43 +			selectedSubtaskId: null
 44 +		};
 45 +
 46 +		while (stack.length > 1 && stack[stack.length - 1].depth >= depth) {
 47 +			stack.pop();
 48 +		}
 49 +
 50 +		if (depth === 0) {
 51 +			Object.assign(root, newTask);
 52 +		} else {
 53 +			stack[stack.length - 1].task.subtasks.push(newTask);
 54 +		}
 55 +		stack.push({ task: newTask, depth: depth });
 56 +	}
 57 +
 58 +	return root;
 59 +}
 60 +
 61 +function loadTasksFromLocalStorage() {
 62 +	var savedTasks = localStorage.getItem('taskTree');
 63 +	if (savedTasks) {
 64 +		console.log('%cloaded tasks from local storage:', "color: green;");
 65 +		console.log(savedTasks);
 66 +		return deserializeTaskTree(savedTasks);
 67 +	} else {
 68 +		return { id: 'root', text: 'todo', state: 0, subtasks: [{ id: generateId(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null };
 69 +	}
 70 +}
 71 +
 72 +function handleSave(e) {
 73 +	if ((e.metaKey || e.ctrlKey) && e.key === 's') {
 74 +		e.preventDefault();
 75 +		saveTaskTreeToFile();
 76 +	}
 77 +}
 78 +
 79 +function saveTaskTreeToFile() {
 80 +	var serializedTasks = serializeTaskTree(state.taskPath[0]);
 81 +	var rootTaskName = state.taskPath[0].text || 'Untitled';
 82 +	var date = new Date();
 83 +	var fileName = `${rootTaskName} - ${date.toLocaleString('default', { month: 'short' }).toLowerCase()} ${date.getDate()}, '${date.getFullYear().toString().slice(-2)}.txt`;
 84 +
 85 +	var blob = new Blob([serializedTasks], { type: 'text/plain' });
 86 +	var url = URL.createObjectURL(blob);
 87 +
 88 +	var a = document.createElement('a');
 89 +	a.href = url;
 90 +	a.download = fileName;
 91 +	a.click();
 92 +
 93 +	URL.revokeObjectURL(url);
 94 +}
 95 +
 96 +function handleOpen(e) {
 97 +	if ((e.metaKey || e.ctrlKey) && e.key === 'o') {
 98 +		e.preventDefault();
 99 +		openTaskTreeFromFile();
100 +	}
101 +}
102 +
103 +function openTaskTreeFromFile() {
104 +	var input = document.createElement('input');
105 +	input.type = 'file';
106 +	input.accept = '.txt';
107 +	input.onchange = function(event) {
108 +		var file = event.target.files[0];
109 +		var reader = new FileReader();
110 +		reader.onload = function(e) {
111 +			try {
112 +				var newRootTask = deserializeTaskTree(e.target.result);
113 +				if (confirm('Are you sure you want to overwrite the existing task tree?')) {
114 +					state.rootTask = newRootTask;
115 +					state.currentTask = state.rootTask;
116 +					state.taskPath = [state.currentTask];
117 +					renderCurrentView();
118 +					saveTasksToLocalStorage();
119 +				}
120 +			} catch (error) {
121 +				alert(`Error importing task tree: ${error.message}`);
122 +			}
123 +		};
124 +		reader.readAsText(file);
125 +	};
126 +	input.click();
127 +}
resources/task-element.js
  1 +// Task element: DOM creation and per-element event handlers
  2 +
  3 +function createTaskElement(task, isParentTask = false) {
  4 +	var taskContainer = document.createElement('div');
  5 +	taskContainer.className = 'task-container';
  6 +	taskContainer.dataset.id = task.id;
  7 +	if (isParentTask) taskContainer.classList.add('parent-task');
  8 +
  9 +	var checkbox = document.createElement('input');
 10 +	checkbox.type = 'checkbox';
 11 +	checkbox.className = 'custom-checkbox';
 12 +	checkbox.id = `checkbox-${task.id}`;
 13 +	updateCheckboxState(checkbox, task.state);
 14 +	checkbox.addEventListener('click', (e) => {
 15 +		e.preventDefault();
 16 +		toggleTaskState(task);
 17 +	});
 18 +
 19 +	var checkboxLabel = document.createElement('label');
 20 +	checkboxLabel.className = 'checkbox-label';
 21 +	checkboxLabel.setAttribute('for', `checkbox-${task.id}`);
 22 +
 23 +	var taskInput = document.createElement('input');
 24 +	taskInput.type = 'text';
 25 +	taskInput.value = task.text;
 26 +	taskInput.setAttribute('autocomplete', 'off');
 27 +
 28 +	taskInput.addEventListener('mousedown', function(e) {
 29 +		e.stopPropagation();
 30 +	});
 31 +
 32 +	var keydownHandler = function(e) {
 33 +		if (e.key === 'Backspace') {
 34 +			if (keyHandler.backspace.blocked) {
 35 +				e.preventDefault();
 36 +				return;
 37 +			}
 38 +			// Multi-select backspace handling
 39 +			if (state.multiSelectedIds.length > 1) {
 40 +				var selected = getMultiSelectedTasks();
 41 +				var allEmpty = selected.every(t => t.text === '');
 42 +				if (allEmpty) {
 43 +					e.preventDefault();
 44 +					if (keyHandler.backspace.canDelete) {
 45 +						keyHandler.backspace.blocked = true;
 46 +						var toDelete = selected.filter(t => t !== state.taskPath[0] && t !== state.currentTask);
 47 +						if (toDelete.length > 0 && !(state.currentTask.id === 'root' && toDelete.length >= state.currentTask.subtasks.length)) {
 48 +							var firstDeleteIdx = Math.min(...toDelete.map(t => state.currentTask.subtasks.findIndex(s => s.id === t.id)).filter(i => i !== -1));
 49 +							for (var t of toDelete) {
 50 +								var idx = state.currentTask.subtasks.findIndex(s => s.id === t.id);
 51 +								if (idx !== -1) state.currentTask.subtasks.splice(idx, 1);
 52 +							}
 53 +							clearMultiSelect();
 54 +							updateTaskAndAncestors(state.currentTask);
 55 +							if (state.currentTask.subtasks.length === 0 && state.taskPath.length > 1) {
 56 +								navigateToParentTask();
 57 +							} else {
 58 +								renderCurrentView();
 59 +								var targetIndex = Math.max(0, firstDeleteIdx - 1);
 60 +								selectAndFocusTask(state.currentTask.subtasks[targetIndex]);
 61 +							}
 62 +							scheduleSave();
 63 +						} else {
 64 +							for (var t of selected) {
 65 +								applyShakeAnimation(t.id);
 66 +							}
 67 +						}
 68 +					}
 69 +					return;
 70 +				}
 71 +				// Some tasks still have text: remove last char from all that have text
 72 +				e.preventDefault();
 73 +				for (var t of selected) {
 74 +					if (t.text.length > 0) {
 75 +						t.text = t.text.slice(0, -1);
 76 +						var inp = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
 77 +						if (inp) inp.value = t.text;
 78 +					}
 79 +				}
 80 +				keyHandler.backspace.canDelete = false;
 81 +				scheduleSave();
 82 +				return;
 83 +			}
 84 +			if (taskInput.value === '' && keyHandler.backspace.canDelete && state.multiSelectedIds.length <= 1) {
 85 +				e.preventDefault();
 86 +				if (task !== state.taskPath[0]) {
 87 +					keyHandler.backspace.blocked = true;
 88 +					clearMultiSelect();
 89 +					if (task === state.currentTask) {
 90 +						deleteCurrentParentTask();
 91 +					} else {
 92 +						deleteSubtask(task);
 93 +					}
 94 +				} else {
 95 +					applyShakeAnimation(task.id);
 96 +				}
 97 +			} else if (taskInput.value !== '') {
 98 +				keyHandler.backspace.canDelete = false;
 99 +			}
100 +		} else if (e.key === 'Enter' && !e.shiftKey) {
101 +			if (keyHandler.enter.blocked) {
102 +				e.preventDefault();
103 +				return;
104 +			}
105 +			if (keyHandler.enter.canAdd) {
106 +				e.preventDefault();
107 +				keyHandler.enter.blocked = true;
108 +				clearMultiSelect();
109 +				addNewSubtask(state.currentTask, task);
110 +			}
111 +		} else if (e.key === 'ArrowDown' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
112 +			if (keyHandler.arrowDown.blocked) {
113 +				e.preventDefault();
114 +				return;
115 +			}
116 +			if (isLastSubtask(task) && state.lastSubtaskDownArrowReleased && task !== state.currentTask) {
117 +				e.preventDefault();
118 +				keyHandler.arrowDown.blocked = true;
119 +				addNewSubtask(state.currentTask, task);
120 +				state.lastSubtaskDownArrowReleased = false;
121 +			} else {
122 +				handleKeyDown(e, task);
123 +			}
124 +		} else if (e.key === 'ArrowDown' && e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
125 +			if (keyHandler.shiftArrowDown.blocked) {
126 +				e.preventDefault();
127 +				return;
128 +			}
129 +			// Multi-select: insert new task above chunk when at bottom
130 +			if (state.multiSelectedIds.length > 1 && !e.repeat) {
131 +				var selected = getMultiSelectedTasks();
132 +				var lastSelected = selected[selected.length - 1];
133 +				if (isLastSubtask(lastSelected) && state.lastSubtaskShiftDownReleased) {
134 +					e.preventDefault();
135 +					keyHandler.shiftArrowDown.blocked = true;
136 +					var topIndex = state.currentTask.subtasks.findIndex(t => state.multiSelectedIds.includes(t.id));
137 +					var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
138 +					state.currentTask.subtasks.splice(topIndex, 0, newSubtask);
139 +					updateTaskAndAncestors(state.currentTask);
140 +					state.currentTask.selectedSubtaskId = state.multiSelectAnchorId;
141 +					renderCurrentView();
142 +					applyMultiSelectHighlights();
143 +					scheduleSave();
144 +					state.lastSubtaskShiftDownReleased = false;
145 +				} else {
146 +					handleKeyDown(e, task);
147 +				}
148 +			} else if (isLastSubtask(task) && state.lastSubtaskShiftDownReleased && task !== state.currentTask && !e.repeat) {
149 +				e.preventDefault();
150 +				keyHandler.shiftArrowDown.blocked = true;
151 +				var parentTask = findParentTask(task);
152 +				if (parentTask) {
153 +					var index = parentTask.subtasks.findIndex(t => t.id === task.id);
154 +					var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
155 +					parentTask.subtasks.splice(index, 0, newSubtask);
156 +					updateTaskAndAncestors(parentTask);
157 +					renderCurrentView();
158 +					selectAndFocusTask(task);
159 +					scheduleSave();
160 +				}
161 +				state.lastSubtaskShiftDownReleased = false;
162 +			} else {
163 +				handleKeyDown(e, task);
164 +			}
165 +		} else {
166 +			handleKeyDown(e, task);
167 +		}
168 +	};
169 +
170 +	var keyupHandler = function(e) {
171 +		if (e.key === 'Backspace') {
172 +			keyHandler.backspace.canDelete = true;
173 +			keyHandler.backspace.blocked = false;
174 +		} else if (e.key === 'Enter') {
175 +			keyHandler.enter.canAdd = true;
176 +			keyHandler.enter.blocked = false;
177 +			keyHandler.shiftEnter.pressed = false;
178 +		} else if (e.key === 'ArrowDown') {
179 +			keyHandler.arrowDown.canAdd = true;
180 +			keyHandler.arrowDown.blocked = false;
181 +			keyHandler.shiftArrowDown.blocked = false;
182 +			var isAtBottom = state.multiSelectedIds.length > 1
183 +				? isLastSubtask(getMultiSelectedTasks().slice(-1)[0])
184 +				: isLastSubtask(task);
185 +			if (isAtBottom) {
186 +				state.lastSubtaskDownArrowReleased = true;
187 +				state.lastSubtaskShiftDownReleased = true;
188 +			} else {
189 +				state.lastSubtaskDownArrowReleased = false;
190 +				state.lastSubtaskShiftDownReleased = false;
191 +			}
192 +		} else if (e.key === 'ArrowRight') {
193 +			keyHandler.shiftRight.pressed = false;
194 +		} else if (e.key === 'ArrowLeft') {
195 +			keyHandler.shiftLeft.pressed = false;
196 +		}
197 +	};
198 +
199 +	taskInput.addEventListener('keydown', keydownHandler);
200 +	taskInput.addEventListener('keyup', keyupHandler);
201 +	taskInput.addEventListener('keydown', handleCopyAndCut);
202 +	taskInput.addEventListener('input', (e) => {
203 +		var oldText = task.text;
204 +		task.text = taskInput.value;
205 +		// Propagate edits to all other multi-selected tasks
206 +		if (state.multiSelectedIds.length > 1 && state.multiSelectedIds.includes(task.id)) {
207 +			var otherSelected = getMultiSelectedTasks().filter(t => t.id !== task.id);
208 +
209 +			if (e.inputType === 'insertText' && e.data) {
210 +				for (var t of otherSelected) {
211 +					t.text = t.text + e.data;
212 +					var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
213 +					if (otherInput) otherInput.value = t.text;
214 +				}
215 +			} else if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
216 +				for (var t of otherSelected) {
217 +					if (t.text.length > 0) {
218 +						t.text = t.text.slice(0, -1);
219 +					}
220 +					var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
221 +					if (otherInput) otherInput.value = t.text;
222 +				}
223 +			} else if (e.inputType === 'insertFromPaste' || e.inputType === 'insertFromDrop') {
224 +				var addedLen = taskInput.value.length - oldText.length;
225 +				if (addedLen > 0) {
226 +					var pastedText = taskInput.value.slice(taskInput.selectionStart - addedLen, taskInput.selectionStart);
227 +					for (var t of otherSelected) {
228 +						t.text = t.text + pastedText;
229 +						var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
230 +						if (otherInput) otherInput.value = t.text;
231 +					}
232 +				}
233 +			}
234 +		}
235 +		if (task === state.currentTask) {
236 +			updatePageTitle(task);
237 +		}
238 +		scheduleSave();
239 +	});
240 +
241 +	taskInput.addEventListener('focus', () => {
242 +		if (state.multiSelectedIds.length > 1 && !state.multiSelectedIds.includes(task.id)) {
243 +			clearMultiSelect();
244 +		}
245 +		setActiveTask(taskInput, task);
246 +	});
247 +
248 +	taskContainer.appendChild(checkbox);
249 +	taskContainer.appendChild(checkboxLabel);
250 +	taskContainer.appendChild(taskInput);
251 +
252 +	return taskContainer;
253 +}
resources/task-model.js
  1 +// Task model: state calculations, checkbox logic, tree traversal
  2 +
  3 +function updateCheckboxState(checkbox, taskState) {
  4 +	checkbox.checked = taskState === 1;
  5 +	checkbox.indeterminate = taskState === 2;
  6 +}
  7 +
  8 +function toggleTaskState(task) {
  9 +	// Save cursor position before re-rendering
 10 +	var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
 11 +	var cursorPos = taskInput ? taskInput.selectionStart : null;
 12 +
 13 +	if (task.state === 1) {
 14 +		if (task.subtasks.length === 0 || !task.subtasks.every(t => t.state === 1)) {
 15 +			task.state = 0;
 16 +			updateSubtasksState(task, 0);
 17 +
 18 +			var parent = findParentTask(task);
 19 +			if (parent) {
 20 +				updateTaskAndAncestors(parent);
 21 +			}
 22 +
 23 +			renderCurrentView();
 24 +			selectAndFocusTask(task, cursorPos);
 25 +			scheduleSave();
 26 +		} else {
 27 +			applyShakeAnimation(task.id);
 28 +		}
 29 +	} else {
 30 +		task.state = 1;
 31 +		updateSubtasksState(task, 1);
 32 +
 33 +		var parent = findParentTask(task);
 34 +		if (parent) {
 35 +			updateTaskAndAncestors(parent);
 36 +		}
 37 +
 38 +		renderCurrentView();
 39 +		selectAndFocusTask(task, cursorPos);
 40 +		scheduleSave();
 41 +	}
 42 +}
 43 +
 44 +function recalculateTaskState(task) {
 45 +	if (task.subtasks.length === 0) {
 46 +		return task.state;
 47 +	}
 48 +	var anyUnchecked = task.subtasks.some(t => t.state === 0);
 49 +	var allChecked = task.subtasks.every(t => t.state === 1);
 50 +	var allCheckedOrIndeterminate = task.subtasks.every(t => t.state === 1 || t.state === 2);
 51 +
 52 +	if (allChecked) {
 53 +		return 1;
 54 +	} else if (allCheckedOrIndeterminate) {
 55 +		return task.state === 2 ? 2 : 1;
 56 +	} else if (anyUnchecked) {
 57 +		return 0;
 58 +	}
 59 +}
 60 +
 61 +function updateTaskAndAncestors(task) {
 62 +	var newState = recalculateTaskState(task);
 63 +	if (task.state !== newState) {
 64 +		var oldState = task.state;
 65 +		task.state = newState;
 66 +
 67 +		if (oldState === 1 && newState === 0) {
 68 +			updateSubtasksState(task, 0);
 69 +		}
 70 +
 71 +		var parent = findParentTask(task);
 72 +		if (parent) {
 73 +			updateTaskAndAncestors(parent);
 74 +		}
 75 +	}
 76 +}
 77 +
 78 +function updateSubtasksState(task, newState) {
 79 +	task.subtasks.forEach(subtask => {
 80 +		if (subtask.state !== 1) {
 81 +			if (newState === 1) {
 82 +				subtask.state = subtask.state === 0 ? 2 : subtask.state;
 83 +			} else if (newState === 0) {
 84 +				subtask.state = 0;
 85 +			}
 86 +			if (subtask.subtasks.length > 0) {
 87 +				updateSubtasksState(subtask, newState);
 88 +			}
 89 +		}
 90 +	});
 91 +}
 92 +
 93 +function adjustMovedTaskState(movedTask, newParent) {
 94 +	if (movedTask.state === 2 && newParent.state === 0) {
 95 +		movedTask.state = 0;
 96 +		updateSubtasksState(movedTask, 0);
 97 +	}
 98 +}
 99 +
100 +function findParentTask(task) {
101 +	for (var i = state.taskPath.length - 1; i >= 0; i--) {
102 +		var potentialParent = state.taskPath[i];
103 +		if (potentialParent.subtasks.some(t => t.id === task.id)) {
104 +			return potentialParent;
105 +		}
106 +	}
107 +	return null;
108 +}
109 +
110 +function isLastSubtask(task) {
111 +	var parentTask = findParentTask(task);
112 +	if (!parentTask) return false;
113 +	return parentTask.subtasks[parentTask.subtasks.length - 1].id === task.id;
114 +}
115 +
116 +function addNewSubtask(parentTask, currentSubtask = null) {
117 +	var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
118 +	if (currentSubtask) {
119 +		var index = parentTask.subtasks.findIndex(t => t.id === currentSubtask.id);
120 +		parentTask.subtasks.splice(index + 1, 0, newSubtask);
121 +	} else {
122 +		parentTask.subtasks.push(newSubtask);
123 +	}
124 +
125 +	updateTaskAndAncestors(parentTask);
126 +	renderCurrentView();
127 +	selectAndFocusTask(newSubtask);
128 +	scheduleSave();
129 +}
130 +
131 +function deleteSubtask(subtask) {
132 +	var parentTask = state.taskPath[state.taskPath.length - 1];
133 +	var index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
134 +
135 +	if (parentTask.id === 'root' && parentTask.subtasks.length === 1) {
136 +		applyShakeAnimation(subtask.id);
137 +		return;
138 +	}
139 +
140 +	parentTask.subtasks = parentTask.subtasks.filter(t => t.id !== subtask.id);
141 +	updateTaskAndAncestors(parentTask);
142 +
143 +	if (parentTask.subtasks.length === 0 && state.taskPath.length > 1) {
144 +		navigateToParentTask();
145 +	} else {
146 +		renderCurrentView();
147 +		if (parentTask.subtasks.length > 0) {
148 +			var targetIndex = Math.max(0, index - 1);
149 +			selectAndFocusTask(parentTask.subtasks[targetIndex]);
150 +		} else {
151 +			selectAndFocusTask(parentTask);
152 +		}
153 +	}
154 +	scheduleSave();
155 +}
156 +
157 +function deleteCurrentParentTask() {
158 +	if (state.taskPath.length <= 1) return;
159 +
160 +	var currentParentTask = state.taskPath[state.taskPath.length - 1];
161 +	var grandparentTask = state.taskPath[state.taskPath.length - 2];
162 +
163 +	if (grandparentTask.id === 'root' && grandparentTask.subtasks.length === 1) {
164 +		applyShakeAnimation(currentParentTask.id);
165 +		return;
166 +	}
167 +
168 +	var index = grandparentTask.subtasks.findIndex(t => t.id === currentParentTask.id);
169 +
170 +	grandparentTask.subtasks = grandparentTask.subtasks.filter(t => t.id !== currentParentTask.id);
171 +	updateTaskAndAncestors(grandparentTask);
172 +
173 +	state.taskPath.pop();
174 +	state.currentTask = grandparentTask;
175 +
176 +	if (grandparentTask.subtasks.length === 0 && state.taskPath.length > 1) {
177 +		navigateToParentTask();
178 +	} else {
179 +		renderCurrentView();
180 +		if (grandparentTask.subtasks.length > 0) {
181 +			var targetIndex = Math.max(0, index - 1);
182 +			selectAndFocusTask(grandparentTask.subtasks[targetIndex]);
183 +		} else {
184 +			selectAndFocusTask(grandparentTask);
185 +		}
186 +	}
187 +}
resources/theme.js
 1 +// Theme: theme detection, cycling, and persistence
 2 +
 3 +function getThemesFromCSS() {
 4 +	for (var sheet of document.styleSheets) {
 5 +		try {
 6 +			for (var rule of sheet.cssRules) {
 7 +				var match = rule.selectorText && rule.selectorText.match(/:root\[data-theme="([^"]+)"\]/);
 8 +				if (match) {
 9 +					state.themes.push(match[1]);
10 +				}
11 +			}
12 +		} catch (e) {
13 +			// Skip cross-origin stylesheets
14 +		}
15 +	}
16 +}
17 +
18 +function cycleTheme() {
19 +	state.currentThemeIndex = (state.currentThemeIndex + 1) % state.themes.length;
20 +	var newTheme = state.themes[state.currentThemeIndex];
21 +	setTheme(newTheme);
22 +	saveThemeToLocalStorage(newTheme);
23 +}
24 +
25 +function saveThemeToLocalStorage(theme) {
26 +	localStorage.setItem('currentTheme', theme);
27 +}
28 +
29 +function updateFavicon() {
30 +	var iconEmoji = getComputedStyle(document.documentElement).getPropertyValue('--icon').trim().replace(/'/g, '');
31 +	var faviconLink = document.querySelector('link[rel="icon"]');
32 +	if (faviconLink && iconEmoji) {
33 +		faviconLink.href = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>${iconEmoji}</text></svg>`;
34 +	}
35 +}
36 +
37 +function setTheme(theme) {
38 +	document.documentElement.setAttribute('data-theme', theme);
39 +	var backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
40 +	document.getElementById('themeColor').setAttribute('content', backgroundColor);
41 +	updateFavicon();
42 +}
43 +
44 +function setInitialTheme() {
45 +	var savedTheme = localStorage.getItem('currentTheme');
46 +	var defaultTheme = document.documentElement.getAttribute('data-theme');
47 +
48 +	if (savedTheme && state.themes.includes(savedTheme)) {
49 +		state.currentThemeIndex = state.themes.indexOf(savedTheme);
50 +	} else if (state.themes.includes(defaultTheme)) {
51 +		state.currentThemeIndex = state.themes.indexOf(defaultTheme);
52 +	} else {
53 +		state.currentThemeIndex = 0;
54 +	}
55 +
56 +	setTheme(state.themes[state.currentThemeIndex]);
57 +}
sw.js
 1 +const CACHE_NAME = 'matryoshka-v1';
 2 +const ASSETS = [
 3 +	'./',
 4 +	'index.html',
 5 +	'manifest.json',
 6 +	'resources/main.css',
 7 +	'resources/state.js',
 8 +	'resources/storage.js',
 9 +	'resources/task-model.js',
10 +	'resources/multi-select.js',
11 +	'resources/navigation.js',
12 +	'resources/reorganize.js',
13 +	'resources/keyboard.js',
14 +	'resources/render.js',
15 +	'resources/task-element.js',
16 +	'resources/theme.js',
17 +	'resources/main.js',
18 +	'resources/icons/icon.svg',
19 +	'resources/fonts/Basteleur-Moonlight.ttf',
20 +];
21 +
22 +// Cache assets on install
23 +self.addEventListener('install', (event) => {
24 +	event.waitUntil(
25 +		caches.open(CACHE_NAME)
26 +			.then((cache) => cache.addAll(ASSETS))
27 +			.then(() => self.skipWaiting())
28 +	);
29 +});
30 +
31 +// Clean up old caches on activate
32 +self.addEventListener('activate', (event) => {
33 +	event.waitUntil(
34 +		caches.keys()
35 +			.then((keys) => Promise.all(
36 +				keys.filter((key) => key !== CACHE_NAME)
37 +					.map((key) => caches.delete(key))
38 +			))
39 +			.then(() => self.clients.claim())
40 +	);
41 +});
42 +
43 +// Serve from cache, fall back to network, and update cache
44 +self.addEventListener('fetch', (event) => {
45 +	event.respondWith(
46 +		caches.match(event.request).then((cached) => {
47 +			const fetchPromise = fetch(event.request).then((response) => {
48 +				// Only cache same-origin, successful GET requests
49 +				if (response.ok && event.request.method === 'GET') {
50 +					const clone = response.clone();
51 +					caches.open(CACHE_NAME).then((cache) => {
52 +						cache.put(event.request, clone);
53 +					});
54 +				}
55 +				return response;
56 +			}).catch(() => cached);
57 +
58 +			return cached || fetchPromise;
59 +		})
60 +	);
61 +});