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>
144 lines
6.4 KiB
Markdown
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)
|