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>
6.4 KiB
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.tsminimal: 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 implementationsui/for modals, views, and UI componentsutils/for helper functions
Plugin Architecture
- Extends
Pluginclass fromobsidianpackage - 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.mjsexternal array; never bundle these - Mobile compatibility: Avoid Node/Electron APIs unless
isDesktopOnly: truein 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
idafter initial release - Use semantic versioning for
version - Keep
minAppVersionaccurate when using newer Obsidian APIs - Update both
manifest.jsonandversions.jsonwhen bumping versions
Development Workflow
Testing Locally
- Build the plugin:
npm run devornpm run build - Copy
main.js,manifest.json,styles.cssto:<Vault>/.obsidian/plugins/<plugin-id>/ - Reload Obsidian and enable plugin in Settings → Community plugins
Type Checking
- TypeScript strict mode enabled in
tsconfig.json - Build command runs
tsc -noEmit -skipLibCheckbefore bundling - Fix type errors before considering build successful
Important Files
src/main.ts- Plugin class and lifecyclesrc/settings.ts- Settings interface, defaults, and settings tab UImanifest.json- Plugin metadata (never commit with wrong version)esbuild.config.mjs- Build configurationeslint.config.mts- ESLint configuration with Obsidian-specific rulesstyles.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/awaitover 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 (TodoSidebarViewextendsItemView)src/views/todo-item-component.ts— Pure DOM factory for individual todo<li>elementssrc/core/todo-parser.ts— Parses markdown intoTodoGroup[]with nestedTodoItemtreessrc/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 notesrc/modals/heading-select-modal.ts— Second step: pick heading within target note
Data Flow
parseTodosGroupedByHeading(content)→TodoGroup[](each group has a heading + tree of todos)buildTodoTree(flatTodos)— stack-based algorithm: todos whose indent > parent's become childrenrenderGroups()renders each group;createTodoItemEl()renders items recursivelyflatTodoList: FlatTodoEntry[]is rebuilt each render for keyboard navigation
Event Listener Lifecycle (Critical)
- Register keydown listener ONCE in
onOpen(), not inrenderGroups()— otherwise listeners accumulate on every file change and arrow keys skip items - Use
this.currentFileinside handlers instead of closure variables (file changes between renders) registerEvent()wrappers handle cleanup automatically on view close
Keyboard Navigation
focusedIndex = -1means panel is not yet activated (no visual focus)- First ArrowDown/ArrowUp when
focusedIndex === -1sets it to 0 (activates focus like Outline view) flatTodoListflattens 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>aredraggable="true";stopPropagationondragstartprevents child drag from bubbling to parent handleDragStartstoresdraggedTodoanddraggedChildLines(viacollectChildLineNumbers)- 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
performNestadjusts indentation withindentTodoLines(lines, delta)before inserting
Hotkey Matching (Platform-Aware)
- Uses
app.hotkeyManager.getHotkeys(commandId)andgetDefaultHotkeys(commandId)(internal API) Platform.isMacOSdistinguishes Mod (Cmd on Mac, Ctrl on Windows) from Ctrl- See
matchesHotkey()intodo-sidebar-view.tsfor the full matching logic
Cross-File Move
- Always pass
addBlankLine: truetoinsertTodoUnderHeadingfrom modals — inserts a blank line between the heading and the moved todo for readability - In-file drag moves use
moveTodoWithChildrendirectly (no blank line needed)