Files
obsidian-todo-tracker/CLAUDE.md
Brendan Chen a00b96231c
Some checks failed
Node.js build / build (22.x) (push) Has been cancelled
Node.js build / build (20.x) (push) Has been cancelled
Add focus activation, 3-zone drag nesting, and click-to-navigate
Implement remaining Round 3 enhancements:
- ArrowDown when panel unfocused activates it at first item (like Outline view)
- 3-zone drag-drop: top/bottom thirds insert above/below, middle third nests as child
- Click on todo text to focus it in editor (onClick callback)
- Dragging parent automatically moves nested children (stopPropagation fix)
- Cross-file move inserts todo below heading with blank line (addBlankLine param)
- Updated CLAUDE.md with sidebar view architecture documentation

Build: 85 tests pass, production build succeeds.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 10:17:27 -08:00

6.4 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Type

This is an Obsidian community plugin written in TypeScript and bundled to JavaScript using esbuild.

Build Commands

# Install dependencies
npm install

# Development mode (watch mode, auto-recompiles on changes)
npm run dev

# Production build (type checks, then bundles with minification)
npm run build

# Lint code
npm run lint

Architecture

Entry Points & Build Process

  • Source code lives in src/
  • Main entry point: src/main.ts (plugin lifecycle management)
  • Build target: main.js (bundled output at root)
  • Bundler: esbuild (configured in esbuild.config.mjs)
  • Required release artifacts: main.js, manifest.json, styles.css (if present)

Code Organization Pattern

  • Keep main.ts minimal: Only plugin lifecycle (onload, onunload, command registration)
  • Delegate feature logic to separate modules
  • Settings are defined in src/settings.ts
  • Organize larger features into subdirectories:
    • commands/ for command implementations
    • ui/ for modals, views, and UI components
    • utils/ for helper functions

Plugin Architecture

  • Extends Plugin class from obsidian package
  • Settings are loaded/saved using this.loadData() / this.saveData()
  • All event listeners, intervals, and DOM events must use this.register*() helpers for proper cleanup
  • Commands are registered via this.addCommand() with stable IDs

Key Constraints

Obsidian-Specific

  • Bundle everything: No external runtime dependencies (except obsidian, electron, CodeMirror packages)
  • External packages: Listed in esbuild.config.mjs external array; never bundle these
  • Mobile compatibility: Avoid Node/Electron APIs unless isDesktopOnly: true in manifest
  • Network requests: Default to offline; require explicit user consent for any external calls
  • No remote code execution: Never fetch/eval scripts or auto-update outside normal releases

Manifest (manifest.json)

  • Never change id after initial release
  • Use semantic versioning for version
  • Keep minAppVersion accurate when using newer Obsidian APIs
  • Update both manifest.json and versions.json when bumping versions

Development Workflow

Testing Locally

  1. Build the plugin: npm run dev or npm run build
  2. Copy main.js, manifest.json, styles.css to:
    <Vault>/.obsidian/plugins/<plugin-id>/
    
  3. Reload Obsidian and enable plugin in Settings → Community plugins

Type Checking

  • TypeScript strict mode enabled in tsconfig.json
  • Build command runs tsc -noEmit -skipLibCheck before bundling
  • Fix type errors before considering build successful

Important Files

  • src/main.ts - Plugin class and lifecycle
  • src/settings.ts - Settings interface, defaults, and settings tab UI
  • manifest.json - Plugin metadata (never commit with wrong version)
  • esbuild.config.mjs - Build configuration
  • eslint.config.mts - ESLint configuration with Obsidian-specific rules
  • styles.css - Optional plugin styles

Code Quality Guidelines

From AGENTS.md and Obsidian best practices:

  • Split files when they exceed ~200-300 lines
  • Use clear module boundaries (single responsibility per file)
  • Prefer async/await over promise chains
  • Register all cleanup via this.register*() helpers to prevent memory leaks
  • Keep startup lightweight; defer heavy work until needed
  • Use stable command IDs (don't rename after release)
  • Provide sensible defaults for all settings

Sidebar View Architecture

Key Files

  • src/views/todo-sidebar-view.ts — Main sidebar panel (TodoSidebarView extends ItemView)
  • src/views/todo-item-component.ts — Pure DOM factory for individual todo <li> elements
  • src/core/todo-parser.ts — Parses markdown into TodoGroup[] with nested TodoItem trees
  • src/core/todo-transformer.ts — Pure functions for modifying file content (toggle, move, indent)
  • src/modals/note-select-modal.ts — First step of cross-file move: pick target note
  • src/modals/heading-select-modal.ts — Second step: pick heading within target note

Data Flow

  1. parseTodosGroupedByHeading(content)TodoGroup[] (each group has a heading + tree of todos)
  2. buildTodoTree(flatTodos) — stack-based algorithm: todos whose indent > parent's become children
  3. renderGroups() renders each group; createTodoItemEl() renders items recursively
  4. flatTodoList: FlatTodoEntry[] is rebuilt each render for keyboard navigation

Event Listener Lifecycle (Critical)

  • Register keydown listener ONCE in onOpen(), not in renderGroups() — otherwise listeners accumulate on every file change and arrow keys skip items
  • Use this.currentFile inside handlers instead of closure variables (file changes between renders)
  • registerEvent() wrappers handle cleanup automatically on view close

Keyboard Navigation

  • focusedIndex = -1 means panel is not yet activated (no visual focus)
  • First ArrowDown/ArrowUp when focusedIndex === -1 sets it to 0 (activates focus like Outline view)
  • flatTodoList flattens the entire tree depth-first; arrow keys walk this list linearly
  • Container has tabindex="0" so it can receive keyboard events without stealing focus on render

Drag and Drop

  • Parent <li> and all nested <li> are draggable="true"; stopPropagation on dragstart prevents child drag from bubbling to parent
  • handleDragStart stores draggedTodo and draggedChildLines (via collectChildLineNumbers)
  • Item drop zones use 3 zones (top/middle/bottom thirds):
    • Top → insert above
    • Middle → nest as child (increase indent, insert after target's last descendant)
    • Bottom → insert below
  • Group (heading) drop zones allow dropping onto the heading area to move to end of that section
  • performNest adjusts indentation with indentTodoLines(lines, delta) before inserting

Hotkey Matching (Platform-Aware)

  • Uses app.hotkeyManager.getHotkeys(commandId) and getDefaultHotkeys(commandId) (internal API)
  • Platform.isMacOS distinguishes Mod (Cmd on Mac, Ctrl on Windows) from Ctrl
  • See matchesHotkey() in todo-sidebar-view.ts for the full matching logic

Cross-File Move

  • Always pass addBlankLine: true to insertTodoUnderHeading from modals — inserts a blank line between the heading and the moved todo for readability
  • In-file drag moves use moveTodoWithChildren directly (no blank line needed)