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

144 lines
6.4 KiB
Markdown

# 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
```bash
# 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)