diff --git a/src/core/todo-transformer.test.ts b/src/core/todo-transformer.test.ts index ed5d3ec..9b6f6a8 100644 --- a/src/core/todo-transformer.test.ts +++ b/src/core/todo-transformer.test.ts @@ -7,6 +7,9 @@ import { moveTodoWithChildren, moveTodoUnderHeadingInFile, indentTodoLines, + collectTodoBlockLines, + removeTodoBlock, + moveTodoBlock, } from './todo-transformer'; describe('toggleTodo', () => { @@ -315,3 +318,99 @@ describe('insertTodoUnderHeading with blank line', () => { expect(result).toBe('# Heading\n\n- [ ] New todo\nExisting content'); }); }); + +describe('collectTodoBlockLines', () => { + it('should collect checkbox children only', () => { + const content = '- [ ] Parent\n - [ ] Child\n - [ ] Child 2'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([1, 2]); + }); + + it('should collect non-checkbox lines with deeper indentation', () => { + const content = '- [ ] Parent\n - Non-checkbox bullet\n - [ ] Child'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([1, 2]); + }); + + it('should stop at same-level indent', () => { + const content = '- [ ] First\n - [ ] Child\n- [ ] Second'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([1]); + }); + + it('should stop at blank line', () => { + const content = '- [ ] First\n - [ ] Child\n\n- [ ] Next'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([1]); + }); + + it('should return empty array for todo with no children', () => { + const content = '- [ ] Solo\n- [ ] Next'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([]); + }); + + it('should handle nested indentation levels', () => { + const content = '- [ ] A\n - B\n - C\n - D\n - E'; + const result = collectTodoBlockLines(content, 0); + expect(result).toEqual([1, 2, 3, 4]); + }); +}); + +describe('removeTodoBlock', () => { + it('should remove todo with no children', () => { + const content = '- [ ] Remove\n- [ ] Keep'; + const result = removeTodoBlock(content, 0); + expect(result).toBe('- [ ] Keep'); + }); + + it('should remove todo and all its children', () => { + const content = '- [ ] Remove\n - Child 1\n - Child 2\n- [ ] Keep'; + const result = removeTodoBlock(content, 0); + expect(result).toBe('- [ ] Keep'); + }); + + it('should remove todo from middle of content', () => { + const content = '- [ ] First\n- [ ] Remove\n - Child\n- [ ] Last'; + const result = removeTodoBlock(content, 1); + expect(result).toBe('- [ ] First\n- [ ] Last'); + }); + + it('should handle nested mixed content', () => { + const content = '- [ ] Parent\n - Bullet\n - [ ] Checkbox child\n- [ ] Next'; + const result = removeTodoBlock(content, 0); + expect(result).toBe('- [ ] Next'); + }); +}); + +describe('moveTodoBlock', () => { + it('should move todo without children to end', () => { + const content = '- [ ] Move\n- [ ] A\n- [ ] B'; + const result = moveTodoBlock(content, 0, 3); + expect(result).toBe('- [ ] A\n- [ ] B\n- [ ] Move'); + }); + + it('should move todo with children as a block', () => { + const content = '- [ ] Parent\n - Child\n - [ ] Nested\n- [ ] Other'; + const result = moveTodoBlock(content, 0, 4); + expect(result).toBe('- [ ] Other\n- [ ] Parent\n - Child\n - [ ] Nested'); + }); + + it('should adjust indentation with positive delta', () => { + const content = '- [ ] Todo\n - Child\n- [ ] Target'; + const result = moveTodoBlock(content, 0, 3, 1); + expect(result).toBe('- [ ] Target\n - [ ] Todo\n - Child'); + }); + + it('should adjust indentation with negative delta', () => { + const content = ' - [ ] Todo\n - Child\n- [ ] Target'; + const result = moveTodoBlock(content, 0, 3, -1); + expect(result).toBe('- [ ] Target\n- [ ] Todo\n - Child'); + }); + + it('should move within same document without losing children', () => { + const content = '- [ ] A\n- [ ] B\n - B1\n - B2\n- [ ] C'; + const result = moveTodoBlock(content, 1, 0); + expect(result).toBe('- [ ] B\n - B1\n - B2\n- [ ] A\n- [ ] C'); + }); +}); diff --git a/src/core/todo-transformer.ts b/src/core/todo-transformer.ts index fca4d49..d9095dc 100644 --- a/src/core/todo-transformer.ts +++ b/src/core/todo-transformer.ts @@ -1,3 +1,5 @@ +import { parseIndentLevel } from './todo-parser'; + /** * Regex pattern for markdown todo checkboxes (for toggling). */ @@ -165,21 +167,22 @@ export function indentTodoLines(todoLines: string[], indentDelta: number): strin } /** - * Insert a todo line under a specific heading. + * Insert a todo line (or block of lines) under a specific heading. * @param content - The file content * @param headingLineNumber - The line number of the heading (0-based) - * @param todoLine - The todo line to insert + * @param todoLine - The todo line(s) to insert (single string or array of strings) * @param nextHeadingLineNumber - Optional line number of the next heading (for section boundary) * @param addBlankLine - Whether to add a blank line between the heading and the todo */ export function insertTodoUnderHeading( content: string, headingLineNumber: number, - todoLine: string, + todoLine: string | string[], nextHeadingLineNumber?: number, addBlankLine = false ): string { const lines = content.split('\n'); + const todoLines = Array.isArray(todoLine) ? todoLine : [todoLine]; // Determine where to insert let insertPosition: number; @@ -197,14 +200,104 @@ export function insertTodoUnderHeading( if (alreadyHasBlank) { // Insert after the existing blank line - lines.splice(headingLineNumber + 2, 0, todoLine); + lines.splice(headingLineNumber + 2, 0, ...todoLines); } else { - // Insert blank line + todo - lines.splice(insertPosition, 0, '', todoLine); + // Insert blank line + todos + lines.splice(insertPosition, 0, '', ...todoLines); } } else { - lines.splice(insertPosition, 0, todoLine); + lines.splice(insertPosition, 0, ...todoLines); } return lines.join('\n'); } + +/** + * Collect all line numbers that belong to a todo's block (by indentation). + * A line is part of the block if its indentation is greater than the todo's. + * Stops at blank line or line with same/lesser indentation. + */ +export function collectTodoBlockLines(content: string, todoLineNumber: number): number[] { + const lines = content.split('\n'); + const todoLine = lines[todoLineNumber]; + if (!todoLine) return []; + + const todoIndent = parseIndentLevel(todoLine); + const blockLines: number[] = []; + + for (let i = todoLineNumber + 1; i < lines.length; i++) { + const line = lines[i]!; + + // Stop at blank line + if (line.trim() === '') { + break; + } + + // Stop at same or lesser indentation + if (parseIndentLevel(line) <= todoIndent) { + break; + } + + blockLines.push(i); + } + + return blockLines; +} + +/** + * Remove a todo and all its block lines (children, nested content, etc.). + */ +export function removeTodoBlock(content: string, todoLineNumber: number): string { + const blockLines = collectTodoBlockLines(content, todoLineNumber); + const allLineNumbers = [todoLineNumber, ...blockLines].sort((a, b) => a - b); + const lines = content.split('\n'); + + // Remove in reverse to preserve indices + for (let i = allLineNumbers.length - 1; i >= 0; i--) { + lines.splice(allLineNumbers[i]!, 1); + } + + return lines.join('\n'); +} + +/** + * Move a todo block to a new position with optional indentation adjustment. + * Auto-detects all lines in the block by indentation. + * @param indentDelta - Positive = indent, negative = outdent + */ +export function moveTodoBlock( + content: string, + todoLineNumber: number, + targetLineNumber: number, + indentDelta = 0 +): string { + const blockLines = collectTodoBlockLines(content, todoLineNumber); + const allLineNumbers = [todoLineNumber, ...blockLines].sort((a, b) => a - b); + const lines = content.split('\n'); + + // Extract lines to move + let movedLines = allLineNumbers.map((ln) => lines[ln]!); + + // Apply indentation if needed + if (indentDelta !== 0) { + movedLines = indentTodoLines(movedLines, indentDelta); + } + + // Remove original lines (in reverse to preserve indices) + for (let i = allLineNumbers.length - 1; i >= 0; i--) { + lines.splice(allLineNumbers[i]!, 1); + } + + // Adjust target position for removed lines + let adjustedTarget = targetLineNumber; + for (const ln of allLineNumbers) { + if (ln < targetLineNumber) { + adjustedTarget--; + } + } + + // Insert at adjusted target + lines.splice(adjustedTarget, 0, ...movedLines); + + return lines.join('\n'); +} diff --git a/src/modals/heading-select-modal.ts b/src/modals/heading-select-modal.ts index 6f256bd..8876559 100644 --- a/src/modals/heading-select-modal.ts +++ b/src/modals/heading-select-modal.ts @@ -1,6 +1,11 @@ import { App, FuzzySuggestModal, TFile, HeadingCache } from 'obsidian'; import type { TodoItem } from '../core/types'; -import { removeTodoLine, insertTodoAtEnd, insertTodoUnderHeading } from '../core/todo-transformer'; +import { + removeTodoBlock, + insertTodoAtEnd, + insertTodoUnderHeading, + collectTodoBlockLines, +} from '../core/todo-transformer'; interface HeadingOption { heading: HeadingCache | null; @@ -58,11 +63,14 @@ export class HeadingSelectModal extends FuzzySuggestModal { } private async moveTodo(selectedHeading: HeadingCache | null): Promise { - const todoLine = this.todo.rawLine; + // Capture block lines (todo + all its nested content) before removing + let blockLines: string[] = []; - // Remove from source file await this.app.vault.process(this.sourceFile, (content) => { - return removeTodoLine(content, this.todo.lineNumber); + const lines = content.split('\n'); + const blockLineNumbers = [this.todo.lineNumber, ...collectTodoBlockLines(content, this.todo.lineNumber)]; + blockLines = blockLineNumbers.map((ln) => lines[ln]!); + return removeTodoBlock(content, this.todo.lineNumber); }); // Add to target file @@ -78,7 +86,7 @@ export class HeadingSelectModal extends FuzzySuggestModal { return insertTodoUnderHeading( content, selectedHeading.position.start.line, - todoLine, + blockLines, nextHeadingLine, true ); @@ -86,7 +94,9 @@ export class HeadingSelectModal extends FuzzySuggestModal { } else { // End of file await this.app.vault.process(this.targetFile, (content) => { - return insertTodoAtEnd(content, todoLine); + // Reconstruct block as a single string + const blockContent = blockLines.join('\n'); + return insertTodoAtEnd(content, blockContent); }); } } diff --git a/src/modals/note-select-modal.ts b/src/modals/note-select-modal.ts index 6497d14..9afc44c 100644 --- a/src/modals/note-select-modal.ts +++ b/src/modals/note-select-modal.ts @@ -1,7 +1,7 @@ import { App, FuzzySuggestModal, TFile } from 'obsidian'; import type { TodoItem } from '../core/types'; import { HeadingSelectModal } from './heading-select-modal'; -import { removeTodoLine, insertTodoAtEnd } from '../core/todo-transformer'; +import { removeTodoBlock, insertTodoAtEnd, collectTodoBlockLines } from '../core/todo-transformer'; export class NoteSelectModal extends FuzzySuggestModal { private todo: TodoItem; @@ -43,22 +43,27 @@ export class NoteSelectModal extends FuzzySuggestModal { } private async moveTodo(targetFile: TFile, headingLineNumber?: number, nextHeadingLineNumber?: number): Promise { - const todoLine = this.todo.rawLine; + // Capture block lines (todo + all its nested content) before removing + let blockLines: string[] = []; - // Remove from source file await this.app.vault.process(this.sourceFile, (content) => { - return removeTodoLine(content, this.todo.lineNumber); + const lines = content.split('\n'); + const blockLineNumbers = [this.todo.lineNumber, ...collectTodoBlockLines(content, this.todo.lineNumber)]; + blockLines = blockLineNumbers.map((ln) => lines[ln]!); + return removeTodoBlock(content, this.todo.lineNumber); }); // Add to target file if (headingLineNumber !== undefined) { const { insertTodoUnderHeading } = await import('../core/todo-transformer'); await this.app.vault.process(targetFile, (content) => { - return insertTodoUnderHeading(content, headingLineNumber, todoLine, nextHeadingLineNumber, true); + return insertTodoUnderHeading(content, headingLineNumber, blockLines, nextHeadingLineNumber, true); }); } else { await this.app.vault.process(targetFile, (content) => { - return insertTodoAtEnd(content, todoLine); + // Reconstruct block as a single string + const blockContent = blockLines.join('\n'); + return insertTodoAtEnd(content, blockContent); }); } } diff --git a/src/views/todo-item-component.ts b/src/views/todo-item-component.ts index ead53d8..0dae70d 100644 --- a/src/views/todo-item-component.ts +++ b/src/views/todo-item-component.ts @@ -5,8 +5,6 @@ export interface TodoItemCallbacks { onToggle: (todo: TodoItem) => void; onMoveClick: (todo: TodoItem) => void; onClick: (todo: TodoItem) => void; - onDragStart: (evt: DragEvent, todo: TodoItem) => void; - onDragEnd: (evt: DragEvent) => void; } /** @@ -31,7 +29,6 @@ export function createTodoItemEl( callbacks: TodoItemCallbacks ): HTMLElement { const itemEl = container.createEl('li', { cls: 'todo-tracker-item' }); - itemEl.setAttribute('draggable', 'true'); itemEl.dataset.lineNumber = String(todo.lineNumber); // Checkbox @@ -70,17 +67,6 @@ export function createTodoItemEl( menu.showAtMouseEvent(evt); }); - // Drag events — stopPropagation so nested children don't bubble to parent - itemEl.addEventListener('dragstart', (evt) => { - evt.stopPropagation(); - callbacks.onDragStart(evt, todo); - }); - - itemEl.addEventListener('dragend', (evt) => { - evt.stopPropagation(); - callbacks.onDragEnd(evt); - }); - // Render children recursively if (todo.children.length > 0) { const nestedList = itemEl.createEl('ul', { cls: 'todo-tracker-nested-list' }); diff --git a/src/views/todo-sidebar-view.ts b/src/views/todo-sidebar-view.ts index 9810441..767b3be 100644 --- a/src/views/todo-sidebar-view.ts +++ b/src/views/todo-sidebar-view.ts @@ -1,7 +1,7 @@ import { ItemView, Keymap, Platform, TFile, WorkspaceLeaf } from 'obsidian'; import type { TodoItem, TodoGroup } from '../core/types'; import { parseTodosGroupedByHeading } from '../core/todo-parser'; -import { toggleTodo, moveTodoWithChildren, moveTodoUnderHeadingInFile, indentTodoLines } from '../core/todo-transformer'; +import { toggleTodo, collectTodoBlockLines, removeTodoBlock } from '../core/todo-transformer'; import { createTodoItemEl, collectChildLineNumbers } from './todo-item-component'; export const TODO_VIEW_TYPE = 'todo-tracker-view'; @@ -17,8 +17,6 @@ export class TodoSidebarView extends ItemView { private flatTodoList: FlatTodoEntry[] = []; private focusedIndex = -1; private groups: TodoGroup[] = []; - private draggedTodo: TodoItem | null = null; - private draggedChildLines: number[] = []; constructor(leaf: WorkspaceLeaf) { super(leaf); @@ -127,9 +125,6 @@ export class TodoSidebarView extends ItemView { for (const todo of group.todos) { this.renderTodoItem(listEl, todo, file, group); } - - // Set up drop zone on the group - this.setupGroupDropZone(groupEl, group, file); } // Don't auto-focus — user activates via ArrowDown (like Outline view) @@ -150,16 +145,11 @@ export class TodoSidebarView extends ItemView { onToggle: (t) => this.handleToggle(file, t), onMoveClick: (t) => this.handleMoveClick(t, file), onClick: (t) => this.openTodoInEditor(file, t), - onDragStart: (evt, t) => this.handleDragStart(evt, t), - onDragEnd: (evt) => this.handleDragEnd(evt), }); // Register in flat list for keyboard navigation this.flatTodoList.push({ todo, element: itemEl, group }); - // Set up drop target on individual items - this.setupItemDropZone(itemEl, todo, file); - // Recursively register children in flat list for (const child of todo.children) { this.registerChildInFlatList(itemEl, child, group); @@ -177,7 +167,6 @@ export class TodoSidebarView extends ItemView { ) as HTMLElement | null; if (childEl) { this.flatTodoList.push({ todo, element: childEl, group }); - this.setupItemDropZone(childEl, todo, this.currentFile!); for (const grandchild of todo.children) { this.registerChildInFlatList(childEl, grandchild, group); @@ -185,219 +174,6 @@ export class TodoSidebarView extends ItemView { } } - // --- Drag and Drop --- - - private handleDragStart(evt: DragEvent, todo: TodoItem): void { - this.draggedTodo = todo; - this.draggedChildLines = collectChildLineNumbers(todo); - - evt.dataTransfer?.setData('text/plain', todo.text); - if (evt.dataTransfer) { - evt.dataTransfer.effectAllowed = 'move'; - } - - // Add dragging class - const target = evt.target as HTMLElement; - target.addClass('todo-tracker-item-dragging'); - } - - private handleDragEnd(evt: DragEvent): void { - this.draggedTodo = null; - this.draggedChildLines = []; - - // Remove dragging class - const target = evt.target as HTMLElement; - target.removeClass('todo-tracker-item-dragging'); - - // Clean up all drag-over indicators - this.contentEl.querySelectorAll('.todo-tracker-drag-over').forEach((el) => { - el.classList.remove('todo-tracker-drag-over'); - }); - this.contentEl.querySelectorAll('.todo-tracker-drop-above, .todo-tracker-drop-below, .todo-tracker-drop-nest').forEach((el) => { - el.classList.remove('todo-tracker-drop-above'); - el.classList.remove('todo-tracker-drop-below'); - el.classList.remove('todo-tracker-drop-nest'); - }); - } - - private getDropZone(evt: DragEvent, itemEl: HTMLElement): 'above' | 'nest' | 'below' { - const rect = itemEl.getBoundingClientRect(); - const relativeY = evt.clientY - rect.top; - const thirdHeight = rect.height / 3; - - if (relativeY < thirdHeight) return 'above'; - if (relativeY < thirdHeight * 2) return 'nest'; - return 'below'; - } - - private setupItemDropZone(itemEl: HTMLElement, targetTodo: TodoItem, file: TFile): void { - itemEl.addEventListener('dragover', (evt) => { - if (!this.draggedTodo || this.draggedTodo.lineNumber === targetTodo.lineNumber) return; - evt.preventDefault(); - if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = 'move'; - } - - const zone = this.getDropZone(evt, itemEl); - - itemEl.removeClass('todo-tracker-drop-above'); - itemEl.removeClass('todo-tracker-drop-below'); - itemEl.removeClass('todo-tracker-drop-nest'); - - if (zone === 'above') itemEl.addClass('todo-tracker-drop-above'); - else if (zone === 'below') itemEl.addClass('todo-tracker-drop-below'); - else itemEl.addClass('todo-tracker-drop-nest'); - }); - - itemEl.addEventListener('dragleave', () => { - itemEl.removeClass('todo-tracker-drop-above'); - itemEl.removeClass('todo-tracker-drop-below'); - itemEl.removeClass('todo-tracker-drop-nest'); - }); - - itemEl.addEventListener('drop', (evt) => { - evt.preventDefault(); - itemEl.removeClass('todo-tracker-drop-above'); - itemEl.removeClass('todo-tracker-drop-below'); - itemEl.removeClass('todo-tracker-drop-nest'); - - if (!this.draggedTodo || !file) return; - - const zone = this.getDropZone(evt, itemEl); - - if (zone === 'nest') { - this.performNest(file, targetTodo); - } else { - const targetLine = zone === 'above' ? targetTodo.lineNumber : targetTodo.lineNumber + 1; - this.performMove(file, targetLine); - } - }); - } - - private setupGroupDropZone(groupEl: HTMLElement, group: TodoGroup, file: TFile): void { - groupEl.addEventListener('dragover', (evt) => { - if (!this.draggedTodo) return; - - // Only handle if drop is on the group itself, not a child item - const target = evt.target as HTMLElement; - if (target.closest('.todo-tracker-item')) return; - - evt.preventDefault(); - if (evt.dataTransfer) { - evt.dataTransfer.dropEffect = 'move'; - } - groupEl.addClass('todo-tracker-drag-over'); - }); - - groupEl.addEventListener('dragleave', (evt) => { - // Only remove if actually leaving the group - const relatedTarget = evt.relatedTarget as HTMLElement | null; - if (!relatedTarget || !groupEl.contains(relatedTarget)) { - groupEl.removeClass('todo-tracker-drag-over'); - } - }); - - groupEl.addEventListener('drop', (evt) => { - evt.preventDefault(); - groupEl.removeClass('todo-tracker-drag-over'); - - if (!this.draggedTodo || !file || !group.heading) return; - - // Find the next heading's line number for section boundary - const groupIndex = this.groups.indexOf(group); - let nextHeadingLine: number | undefined; - for (let i = groupIndex + 1; i < this.groups.length; i++) { - if (this.groups[i]?.heading) { - nextHeadingLine = this.groups[i]!.heading!.lineNumber; - break; - } - } - - this.performMoveUnderHeading(file, group.heading.lineNumber, nextHeadingLine); - }); - } - - private async performMove(file: TFile, targetLine: number): Promise { - if (!this.draggedTodo) return; - - const todoLine = this.draggedTodo.lineNumber; - const childLines = this.draggedChildLines; - - await this.app.vault.process(file, (content) => { - return moveTodoWithChildren(content, todoLine, childLines, targetLine); - }); - - this.draggedTodo = null; - this.draggedChildLines = []; - } - - private async performNest(file: TFile, targetTodo: TodoItem): Promise { - if (!this.draggedTodo) return; - - const todoLine = this.draggedTodo.lineNumber; - const childLines = this.draggedChildLines; - const targetIndent = targetTodo.indentLevel; - - await this.app.vault.process(file, (content) => { - const lines = content.split('\n'); - const allLineNumbers = [todoLine, ...childLines].sort((a, b) => a - b); - - // Extract lines to move - const movedLines = allLineNumbers.map((ln) => lines[ln]!); - - // Calculate indent delta: we want dragged todo to be one level deeper than target - const draggedIndent = this.draggedTodo!.indentLevel; - const indentDelta = targetIndent + 1 - draggedIndent; - - // Adjust indentation - const indentedLines = indentTodoLines(movedLines, indentDelta); - - // Find insert position: right after target todo and its existing children - const targetChildLines = collectChildLineNumbers(targetTodo); - const allTargetLines = [targetTodo.lineNumber, ...targetChildLines]; - const insertAfter = Math.max(...allTargetLines); - - // Remove original lines (in reverse to preserve indices) - for (let i = allLineNumbers.length - 1; i >= 0; i--) { - lines.splice(allLineNumbers[i]!, 1); - } - - // Adjust insert position for removed lines - let adjustedInsert = insertAfter; - for (const ln of allLineNumbers) { - if (ln <= insertAfter) { - adjustedInsert--; - } - } - - // Insert indented lines after the target - lines.splice(adjustedInsert + 1, 0, ...indentedLines); - - return lines.join('\n'); - }); - - this.draggedTodo = null; - this.draggedChildLines = []; - } - - private async performMoveUnderHeading( - file: TFile, - headingLine: number, - nextHeadingLine?: number - ): Promise { - if (!this.draggedTodo) return; - - const todoLine = this.draggedTodo.lineNumber; - const childLines = this.draggedChildLines; - - await this.app.vault.process(file, (content) => { - return moveTodoUnderHeadingInFile(content, todoLine, childLines, headingLine, nextHeadingLine); - }); - - this.draggedTodo = null; - this.draggedChildLines = []; - } - // --- Keyboard Navigation --- private handleKeydown(evt: KeyboardEvent): void { diff --git a/styles.css b/styles.css index 4561b77..da28ee0 100644 --- a/styles.css +++ b/styles.css @@ -58,7 +58,6 @@ gap: 8px; padding: 6px 4px; border-radius: 4px; - cursor: grab; transition: background-color 0.15s ease; } @@ -90,25 +89,3 @@ background-color: var(--background-modifier-hover); } -/* Drag and drop */ -.todo-tracker-item-dragging { - opacity: 0.4; -} - -.todo-tracker-drop-above { - border-top: 2px solid var(--interactive-accent); -} - -.todo-tracker-drop-below { - border-bottom: 2px solid var(--interactive-accent); -} - -.todo-tracker-drop-nest { - outline: 2px solid var(--interactive-accent); - outline-offset: -2px; - background-color: var(--background-modifier-hover); -} - -.todo-tracker-drag-over { - background-color: var(--background-modifier-hover); -}