From 2936f7d359c86ab2bbbcdde8b49dd2016d425f63 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 19 Feb 2026 16:35:20 -0800 Subject: [PATCH] Initial commit --- .gitignore | 76 +- CLAUDE.md | 97 ++ manifest.json | 6 +- package-lock.json | 1603 +++++++++++++++++++++++++++- package.json | 9 +- src/core/todo-parser.test.ts | 124 +++ src/core/todo-parser.ts | 52 + src/core/todo-transformer.test.ts | 183 ++++ src/core/todo-transformer.ts | 98 ++ src/core/types.ts | 34 + src/main.ts | 116 +- src/modals/heading-select-modal.ts | 92 ++ src/modals/note-select-modal.ts | 65 ++ src/settings.ts | 6 +- src/views/todo-item-component.ts | 55 + src/views/todo-sidebar-view.ts | 117 ++ styles.css | 79 +- vitest.config.ts | 8 + 18 files changed, 2727 insertions(+), 93 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/core/todo-parser.test.ts create mode 100644 src/core/todo-parser.ts create mode 100644 src/core/todo-transformer.test.ts create mode 100644 src/core/todo-transformer.ts create mode 100644 src/core/types.ts create mode 100644 src/modals/heading-select-modal.ts create mode 100644 src/modals/note-select-modal.ts create mode 100644 src/views/todo-item-component.ts create mode 100644 src/views/todo-sidebar-view.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index e09a007..3c1e404 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ # vscode -.vscode +.vscode +.vscode-test # Intellij *.iml .idea # npm -node_modules +node_modules/ # Don't include the compiled main.js file in the repo. # They should be uploaded to GitHub releases instead. @@ -20,3 +21,74 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1143dcf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# 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: + ``` + /.obsidian/plugins// + ``` +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 diff --git a/manifest.json b/manifest.json index dfa940e..e1780c2 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "sample-plugin", - "name": "Sample Plugin", + "id": "todo-tracker", + "name": "Todo Tracker", "version": "1.0.0", "minAppVersion": "0.15.0", - "description": "Demonstrates some of the capabilities of the Obsidian API.", + "description": "Track and manage todos from your notes in a sidebar panel. Toggle checkboxes and move todos between notes.", "author": "Obsidian", "authorUrl": "https://obsidian.md", "fundingUrl": "https://obsidian.md/pricing", diff --git a/package-lock.json b/package-lock.json index d0dac39..4944a27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,14 +13,15 @@ }, "devDependencies": { "@eslint/js": "9.30.1", - "@types/node": "^16.11.6", + "@types/node": "^20.0.0", "esbuild": "0.25.5", "eslint-plugin-obsidianmd": "0.1.9", "globals": "14.0.0", "jiti": "2.6.1", "tslib": "2.4.0", "typescript": "^5.8.3", - "typescript-eslint": "8.35.1" + "typescript-eslint": "8.35.1", + "vitest": "^3.0.0" } }, "node_modules/@codemirror/state": { @@ -403,6 +404,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", @@ -695,6 +713,13 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -791,6 +816,356 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -798,6 +1173,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/codemirror": { "version": "5.60.8", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", @@ -807,6 +1193,13 @@ "@types/tern": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -839,9 +1232,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "16.18.126", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", - "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -1124,6 +1527,121 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1347,6 +1865,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -1404,6 +1932,16 @@ "node": ">=8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1464,6 +2002,23 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1481,6 +2036,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1602,6 +2167,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1814,6 +2389,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2545,6 +3127,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2555,6 +3147,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2713,6 +3315,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3711,6 +4328,23 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3791,6 +4425,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4043,6 +4696,30 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4066,6 +4743,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4252,6 +4958,51 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4520,6 +5271,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -4655,6 +5437,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-mod": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", @@ -4726,6 +5528,98 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4978,6 +5872,688 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -5090,6 +6666,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 17268d7..9283161 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", - "lint": "eslint ." + "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest" }, "keywords": [], "license": "0-BSD", "devDependencies": { - "@types/node": "^16.11.6", + "@types/node": "^20.0.0", "esbuild": "0.25.5", "eslint-plugin-obsidianmd": "0.1.9", "globals": "14.0.0", @@ -21,7 +23,8 @@ "typescript": "^5.8.3", "typescript-eslint": "8.35.1", "@eslint/js": "9.30.1", - "jiti": "2.6.1" + "jiti": "2.6.1", + "vitest": "^3.0.0" }, "dependencies": { "obsidian": "latest" diff --git a/src/core/todo-parser.test.ts b/src/core/todo-parser.test.ts new file mode 100644 index 0000000..b0cbdc9 --- /dev/null +++ b/src/core/todo-parser.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest'; +import { parseTodos } from './todo-parser'; + +describe('parseTodos', () => { + it('should parse unchecked todos with dash marker', () => { + const content = '- [ ] Buy groceries'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]).toEqual({ + id: 'line-0', + text: 'Buy groceries', + completed: false, + lineNumber: 0, + rawLine: '- [ ] Buy groceries', + }); + }); + + it('should parse checked todos with lowercase x', () => { + const content = '- [x] Done task'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.completed).toBe(true); + expect(result.todos[0]?.text).toBe('Done task'); + }); + + it('should parse checked todos with uppercase X', () => { + const content = '- [X] Completed item'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.completed).toBe(true); + }); + + it('should parse multiple todos', () => { + const content = '- [ ] First\n- [x] Second\n- [ ] Third'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(3); + expect(result.todos[0]?.lineNumber).toBe(0); + expect(result.todos[1]?.lineNumber).toBe(1); + expect(result.todos[2]?.lineNumber).toBe(2); + }); + + it('should handle asterisk list marker', () => { + const content = '* [ ] Asterisk todo'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Asterisk todo'); + }); + + it('should handle plus list marker', () => { + const content = '+ [ ] Plus todo'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Plus todo'); + }); + + it('should handle mixed content with non-todo lines', () => { + const content = '# Heading\nSome text\n- [ ] A todo\n- Regular list item\nMore text'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.lineNumber).toBe(2); + expect(result.todos[0]?.text).toBe('A todo'); + }); + + it('should handle indented todos', () => { + const content = ' - [ ] Indented todo'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Indented todo'); + expect(result.todos[0]?.rawLine).toBe(' - [ ] Indented todo'); + }); + + it('should handle tab-indented todos', () => { + const content = '\t- [ ] Tab indented'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Tab indented'); + }); + + it('should return empty array for content with no todos', () => { + const content = '# Just a heading\nSome paragraph text.\n- Regular list item'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(0); + }); + + it('should return empty array for empty content', () => { + const content = ''; + const result = parseTodos(content); + expect(result.todos).toHaveLength(0); + }); + + it('should handle todos with extra whitespace after checkbox', () => { + const content = '- [ ] Extra spaces'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Extra spaces'); + }); + + it('should preserve full rawLine for each todo', () => { + const content = '- [ ] Simple task\n - [x] Nested done'; + const result = parseTodos(content); + expect(result.todos[0]?.rawLine).toBe('- [ ] Simple task'); + expect(result.todos[1]?.rawLine).toBe(' - [x] Nested done'); + }); + + it('should generate unique ids based on line numbers', () => { + const content = '- [ ] First\n\n- [ ] Second'; + const result = parseTodos(content); + expect(result.todos[0]?.id).toBe('line-0'); + expect(result.todos[1]?.id).toBe('line-2'); + }); + + it('should not match checkboxes without proper spacing', () => { + const content = '-[ ] No space before bracket\n-[x]No space after'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(0); + }); + + it('should handle todos with special characters in text', () => { + const content = '- [ ] Task with `code` and **bold** and [link](url)'; + const result = parseTodos(content); + expect(result.todos).toHaveLength(1); + expect(result.todos[0]?.text).toBe('Task with `code` and **bold** and [link](url)'); + }); +}); diff --git a/src/core/todo-parser.ts b/src/core/todo-parser.ts new file mode 100644 index 0000000..056f0e9 --- /dev/null +++ b/src/core/todo-parser.ts @@ -0,0 +1,52 @@ +import type { ParseResult, TodoItem } from './types'; + +/** + * Regex pattern for markdown todo checkboxes. + * Matches: optional whitespace, list marker (-, *, +), space, checkbox [ ] or [x]/[X], space(s), text + */ +const TODO_PATTERN = /^(\s*[-*+]\s+\[)([xX ]?)(\]\s+)(.*)$/; + +/** + * Parse a single line and return a TodoItem if it's a todo, null otherwise. + */ +export function parseTodoLine(line: string, lineNumber: number): TodoItem | null { + const match = line.match(TODO_PATTERN); + if (!match) { + return null; + } + + const checkboxContent = match[2]; + const text = match[4]?.trim() ?? ''; + + return { + id: `line-${lineNumber}`, + text, + completed: checkboxContent === 'x' || checkboxContent === 'X', + lineNumber, + rawLine: line, + }; +} + +/** + * Parse markdown content and extract all todo items. + */ +export function parseTodos(content: string): ParseResult { + if (!content) { + return { todos: [] }; + } + + const lines = content.split('\n'); + const todos: TodoItem[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line === undefined) continue; + + const todo = parseTodoLine(line, i); + if (todo) { + todos.push(todo); + } + } + + return { todos }; +} diff --git a/src/core/todo-transformer.test.ts b/src/core/todo-transformer.test.ts new file mode 100644 index 0000000..44d03ac --- /dev/null +++ b/src/core/todo-transformer.test.ts @@ -0,0 +1,183 @@ +import { describe, it, expect } from 'vitest'; +import { + toggleTodo, + removeTodoLine, + insertTodoAtEnd, + insertTodoUnderHeading, +} from './todo-transformer'; + +describe('toggleTodo', () => { + it('should check an unchecked todo', () => { + const content = '- [ ] Todo item'; + const result = toggleTodo(content, 0); + expect(result).toBe('- [x] Todo item'); + }); + + it('should uncheck a checked todo with lowercase x', () => { + const content = '- [x] Done item'; + const result = toggleTodo(content, 0); + expect(result).toBe('- [ ] Done item'); + }); + + it('should uncheck a checked todo with uppercase X', () => { + const content = '- [X] Done item'; + const result = toggleTodo(content, 0); + expect(result).toBe('- [ ] Done item'); + }); + + it('should preserve surrounding content', () => { + const content = 'Line 1\n- [ ] Todo\nLine 3'; + const result = toggleTodo(content, 1); + expect(result).toBe('Line 1\n- [x] Todo\nLine 3'); + }); + + it('should handle toggling first line', () => { + const content = '- [ ] First\nSecond line'; + const result = toggleTodo(content, 0); + expect(result).toBe('- [x] First\nSecond line'); + }); + + it('should handle toggling last line', () => { + const content = 'First line\n- [ ] Last'; + const result = toggleTodo(content, 1); + expect(result).toBe('First line\n- [x] Last'); + }); + + it('should preserve indentation when toggling', () => { + const content = ' - [ ] Indented'; + const result = toggleTodo(content, 0); + expect(result).toBe(' - [x] Indented'); + }); + + it('should handle asterisk marker', () => { + const content = '* [ ] Asterisk todo'; + const result = toggleTodo(content, 0); + expect(result).toBe('* [x] Asterisk todo'); + }); + + it('should return unchanged content if line is not a todo', () => { + const content = 'Not a todo\n- [ ] Real todo'; + const result = toggleTodo(content, 0); + expect(result).toBe('Not a todo\n- [ ] Real todo'); + }); + + it('should handle single line content', () => { + const content = '- [x] Only line'; + const result = toggleTodo(content, 0); + expect(result).toBe('- [ ] Only line'); + }); +}); + +describe('removeTodoLine', () => { + it('should remove a line from the middle', () => { + const content = 'Line 1\n- [ ] Todo\nLine 3'; + const result = removeTodoLine(content, 1); + expect(result).toBe('Line 1\nLine 3'); + }); + + it('should remove the first line', () => { + const content = '- [ ] First\nSecond\nThird'; + const result = removeTodoLine(content, 0); + expect(result).toBe('Second\nThird'); + }); + + it('should remove the last line', () => { + const content = 'First\nSecond\n- [ ] Last'; + const result = removeTodoLine(content, 2); + expect(result).toBe('First\nSecond'); + }); + + it('should handle single line content', () => { + const content = '- [ ] Only line'; + const result = removeTodoLine(content, 0); + expect(result).toBe(''); + }); + + it('should handle removing from two-line content', () => { + const content = '- [ ] First\n- [ ] Second'; + const result = removeTodoLine(content, 0); + expect(result).toBe('- [ ] Second'); + }); + + it('should handle out of bounds line number gracefully', () => { + const content = 'Line 1\nLine 2'; + const result = removeTodoLine(content, 5); + expect(result).toBe('Line 1\nLine 2'); + }); +}); + +describe('insertTodoAtEnd', () => { + it('should append todo to end of file', () => { + const content = '# Heading\nContent'; + const result = insertTodoAtEnd(content, '- [ ] New todo'); + expect(result).toBe('# Heading\nContent\n- [ ] New todo'); + }); + + it('should handle empty file', () => { + const content = ''; + const result = insertTodoAtEnd(content, '- [ ] New todo'); + expect(result).toBe('- [ ] New todo'); + }); + + it('should handle file ending with newline', () => { + const content = 'Content\n'; + const result = insertTodoAtEnd(content, '- [ ] New todo'); + expect(result).toBe('Content\n- [ ] New todo'); + }); + + it('should handle file ending with multiple newlines', () => { + const content = 'Content\n\n'; + const result = insertTodoAtEnd(content, '- [ ] New todo'); + expect(result).toBe('Content\n\n- [ ] New todo'); + }); + + it('should preserve indentation of the todo being inserted', () => { + const content = 'Content'; + const result = insertTodoAtEnd(content, ' - [ ] Indented todo'); + expect(result).toBe('Content\n - [ ] Indented todo'); + }); +}); + +describe('insertTodoUnderHeading', () => { + it('should insert after heading when no next heading', () => { + const content = '# Heading\nExisting content'; + const result = insertTodoUnderHeading(content, 0, '- [ ] New todo'); + expect(result).toBe('# Heading\n- [ ] New todo\nExisting content'); + }); + + it('should insert before next heading', () => { + const content = '# Heading 1\nContent 1\n# Heading 2\nContent 2'; + const result = insertTodoUnderHeading(content, 0, '- [ ] New todo', 2); + expect(result).toBe('# Heading 1\nContent 1\n- [ ] New todo\n# Heading 2\nContent 2'); + }); + + it('should insert at end of section when section is empty (back to back headings)', () => { + const content = '# Empty Section\n# Next Section'; + const result = insertTodoUnderHeading(content, 0, '- [ ] New todo', 1); + expect(result).toBe('# Empty Section\n- [ ] New todo\n# Next Section'); + }); + + it('should insert after nested heading', () => { + const content = '# H1\n## H2\nContent\n# H1 again'; + const result = insertTodoUnderHeading(content, 1, '- [ ] Under H2', 3); + expect(result).toBe('# H1\n## H2\nContent\n- [ ] Under H2\n# H1 again'); + }); + + it('should handle heading at end of file', () => { + const content = 'Some content\n# Final heading'; + const result = insertTodoUnderHeading(content, 1, '- [ ] New todo'); + expect(result).toBe('Some content\n# Final heading\n- [ ] New todo'); + }); + + it('should handle file with only a heading', () => { + const content = '# Only heading'; + const result = insertTodoUnderHeading(content, 0, '- [ ] New todo'); + expect(result).toBe('# Only heading\n- [ ] New todo'); + }); + + it('should preserve indentation of inserted todo', () => { + const content = '# Heading\nContent'; + const result = insertTodoUnderHeading(content, 0, ' - [ ] Indented'); + expect(result).toBe('# Heading\n - [ ] Indented\nContent'); + }); +}); diff --git a/src/core/todo-transformer.ts b/src/core/todo-transformer.ts new file mode 100644 index 0000000..0d2e2ad --- /dev/null +++ b/src/core/todo-transformer.ts @@ -0,0 +1,98 @@ +/** + * Regex pattern for markdown todo checkboxes (for toggling). + */ +const TODO_PATTERN = /^(\s*[-*+]\s+\[)([xX ]?)(\].*)$/; + +/** + * Toggle the checkbox state at the specified line number. + * Returns the modified content. + */ +export function toggleTodo(content: string, lineNumber: number): string { + const lines = content.split('\n'); + + if (lineNumber < 0 || lineNumber >= lines.length) { + return content; + } + + const line = lines[lineNumber]; + if (line === undefined) { + return content; + } + + const match = line.match(TODO_PATTERN); + if (!match) { + return content; + } + + const checkboxContent = match[2]; + const isChecked = checkboxContent === 'x' || checkboxContent === 'X'; + const newCheckbox = isChecked ? ' ' : 'x'; + + lines[lineNumber] = `${match[1]}${newCheckbox}${match[3]}`; + return lines.join('\n'); +} + +/** + * Remove the line at the specified line number. + * Returns the modified content. + */ +export function removeTodoLine(content: string, lineNumber: number): string { + const lines = content.split('\n'); + + if (lineNumber < 0 || lineNumber >= lines.length) { + return content; + } + + lines.splice(lineNumber, 1); + return lines.join('\n'); +} + +/** + * Insert a todo line at the end of the content. + * Handles files with/without trailing newlines. + */ +export function insertTodoAtEnd(content: string, todoLine: string): string { + if (!content) { + return todoLine; + } + + // If content ends with newline(s), append directly + if (content.endsWith('\n')) { + return content + todoLine; + } + + // Otherwise add a newline before the todo + return content + '\n' + todoLine; +} + +/** + * Insert a todo line 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 nextHeadingLineNumber - Optional line number of the next heading (for section boundary) + */ +export function insertTodoUnderHeading( + content: string, + headingLineNumber: number, + todoLine: string, + nextHeadingLineNumber?: number +): string { + const lines = content.split('\n'); + + // Determine where to insert + let insertPosition: number; + + if (nextHeadingLineNumber !== undefined) { + // Insert just before the next heading (at the end of this section) + insertPosition = nextHeadingLineNumber; + } else { + // No next heading - insert right after the heading line + insertPosition = headingLineNumber + 1; + } + + // Insert the todo line + lines.splice(insertPosition, 0, todoLine); + + return lines.join('\n'); +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..01119c5 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,34 @@ +/** + * Represents a parsed to-do item from markdown + */ +export interface TodoItem { + /** Unique identifier based on line number */ + id: string; + /** The text content after the checkbox */ + text: string; + /** Whether the checkbox is checked */ + completed: boolean; + /** 0-based line number in the source file */ + lineNumber: number; + /** The full original line text (for accurate replacement) */ + rawLine: string; +} + +/** + * Result of parsing a note's content + */ +export interface ParseResult { + todos: TodoItem[]; +} + +/** + * Represents a heading in a note + */ +export interface HeadingInfo { + /** The heading text without # symbols */ + text: string; + /** Heading level (1-6) */ + level: number; + /** 0-based line number */ + lineNumber: number; +} diff --git a/src/main.ts b/src/main.ts index 6fe0c83..64226ef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,76 +1,64 @@ -import {App, Editor, MarkdownView, Modal, Notice, Plugin} from 'obsidian'; -import {DEFAULT_SETTINGS, MyPluginSettings, SampleSettingTab} from "./settings"; +import { Plugin, WorkspaceLeaf } from 'obsidian'; +import { DEFAULT_SETTINGS, MyPluginSettings, SampleSettingTab } from './settings'; +import { TodoSidebarView, TODO_VIEW_TYPE } from './views/todo-sidebar-view'; -// Remember to rename these classes and interfaces! - -export default class MyPlugin extends Plugin { +export default class TodoTrackerPlugin extends Plugin { settings: MyPluginSettings; async onload() { await this.loadSettings(); - // This creates an icon in the left ribbon. - this.addRibbonIcon('dice', 'Sample', (evt: MouseEvent) => { - // Called when the user clicks the icon. - new Notice('This is a notice!'); + // Register the todo sidebar view + this.registerView(TODO_VIEW_TYPE, (leaf: WorkspaceLeaf) => { + return new TodoSidebarView(leaf); }); - // This adds a status bar item to the bottom of the app. Does not work on mobile apps. - const statusBarItemEl = this.addStatusBarItem(); - statusBarItemEl.setText('Status bar text'); + // Add ribbon icon to open todo tracker + this.addRibbonIcon('check-square', 'Open Todo Tracker', () => { + this.activateView(); + }); - // This adds a simple command that can be triggered anywhere + // Add command to toggle todo tracker this.addCommand({ - id: 'open-modal-simple', - name: 'Open modal (simple)', + id: 'open-todo-tracker', + name: 'Open Todo Tracker', callback: () => { - new SampleModal(this.app).open(); - } - }); - // This adds an editor command that can perform some operation on the current editor instance - this.addCommand({ - id: 'replace-selected', - name: 'Replace selected content', - editorCallback: (editor: Editor, view: MarkdownView) => { - editor.replaceSelection('Sample editor command'); - } - }); - // This adds a complex command that can check whether the current state of the app allows execution of the command - this.addCommand({ - id: 'open-modal-complex', - name: 'Open modal (complex)', - checkCallback: (checking: boolean) => { - // Conditions to check - const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView); - if (markdownView) { - // If checking is true, we're simply "checking" if the command can be run. - // If checking is false, then we want to actually perform the operation. - if (!checking) { - new SampleModal(this.app).open(); - } - - // This command will only show up in Command Palette when the check function returns true - return true; - } - return false; - } + this.activateView(); + }, }); - // This adds a settings tab so the user can configure various aspects of the plugin + // Add settings tab this.addSettingTab(new SampleSettingTab(this.app, this)); - - // If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin) - // Using this function will automatically remove the event listener when this plugin is disabled. - this.registerDomEvent(document, 'click', (evt: MouseEvent) => { - new Notice("Click"); - }); - - // When registering intervals, this function will automatically clear the interval when the plugin is disabled. - this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); - } onunload() { + // Detach all leaves of this view type to ensure clean unload + this.app.workspace.detachLeavesOfType(TODO_VIEW_TYPE); + } + + async activateView(): Promise { + const { workspace } = this.app; + + // Check if view already exists + const existingLeaves = workspace.getLeavesOfType(TODO_VIEW_TYPE); + if (existingLeaves.length > 0) { + // Reveal the existing view + const leaf = existingLeaves[0]; + if (leaf) { + workspace.revealLeaf(leaf); + } + return; + } + + // Create new leaf in right sidebar + const leaf = workspace.getRightLeaf(false); + if (leaf) { + await leaf.setViewState({ + type: TODO_VIEW_TYPE, + active: true, + }); + workspace.revealLeaf(leaf); + } } async loadSettings() { @@ -81,19 +69,3 @@ export default class MyPlugin extends Plugin { await this.saveData(this.settings); } } - -class SampleModal extends Modal { - constructor(app: App) { - super(app); - } - - onOpen() { - let {contentEl} = this; - contentEl.setText('Woah!'); - } - - onClose() { - const {contentEl} = this; - contentEl.empty(); - } -} diff --git a/src/modals/heading-select-modal.ts b/src/modals/heading-select-modal.ts new file mode 100644 index 0000000..3eefdcd --- /dev/null +++ b/src/modals/heading-select-modal.ts @@ -0,0 +1,92 @@ +import { App, FuzzySuggestModal, TFile, HeadingCache } from 'obsidian'; +import type { TodoItem } from '../core/types'; +import { removeTodoLine, insertTodoAtEnd, insertTodoUnderHeading } from '../core/todo-transformer'; + +interface HeadingOption { + heading: HeadingCache | null; + displayText: string; +} + +export class HeadingSelectModal extends FuzzySuggestModal { + private todo: TodoItem; + private sourceFile: TFile; + private targetFile: TFile; + private headings: HeadingCache[]; + + constructor( + app: App, + todo: TodoItem, + sourceFile: TFile, + targetFile: TFile, + headings: HeadingCache[] + ) { + super(app); + this.todo = todo; + this.sourceFile = sourceFile; + this.targetFile = targetFile; + this.headings = headings; + this.setPlaceholder('Select a heading (or end of file)...'); + } + + getItems(): HeadingOption[] { + const items: HeadingOption[] = []; + + // Add "End of file" option first + items.push({ + heading: null, + displayText: 'End of file', + }); + + // Add all headings + for (const heading of this.headings) { + const indent = ' '.repeat(heading.level - 1); + items.push({ + heading, + displayText: `${indent}${'#'.repeat(heading.level)} ${heading.heading}`, + }); + } + + return items; + } + + getItemText(option: HeadingOption): string { + return option.displayText; + } + + async onChooseItem(option: HeadingOption): Promise { + await this.moveTodo(option.heading); + } + + private async moveTodo(selectedHeading: HeadingCache | null): Promise { + const todoLine = this.todo.rawLine; + + // Remove from source file + await this.app.vault.process(this.sourceFile, (content) => { + return removeTodoLine(content, this.todo.lineNumber); + }); + + // Add to target file + if (selectedHeading) { + // Find the next heading (if any) to determine section boundary + const headingIndex = this.headings.findIndex( + (h) => h.position.start.line === selectedHeading.position.start.line + ); + const nextHeading = this.headings[headingIndex + 1]; + const nextHeadingLine = nextHeading?.position.start.line; + + await this.app.vault.process(this.targetFile, (content) => { + return insertTodoUnderHeading( + content, + selectedHeading.position.start.line, + todoLine, + nextHeadingLine + ); + }); + } else { + // End of file + await this.app.vault.process(this.targetFile, (content) => { + return insertTodoAtEnd(content, todoLine); + }); + } + } +} diff --git a/src/modals/note-select-modal.ts b/src/modals/note-select-modal.ts new file mode 100644 index 0000000..dfe1cbf --- /dev/null +++ b/src/modals/note-select-modal.ts @@ -0,0 +1,65 @@ +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'; + +export class NoteSelectModal extends FuzzySuggestModal { + private todo: TodoItem; + private sourceFile: TFile; + + constructor(app: App, todo: TodoItem, sourceFile: TFile) { + super(app); + this.todo = todo; + this.sourceFile = sourceFile; + this.setPlaceholder('Select a note to move the todo to...'); + } + + getItems(): TFile[] { + return this.app.vault.getMarkdownFiles(); + } + + getItemText(file: TFile): string { + return file.path; + } + + async onChooseItem(file: TFile): Promise { + // Get headings from the target file + const fileCache = this.app.metadataCache.getFileCache(file); + const headings = fileCache?.headings ?? []; + + if (headings.length > 0) { + // Show heading selection modal + new HeadingSelectModal( + this.app, + this.todo, + this.sourceFile, + file, + headings + ).open(); + } else { + // No headings - move directly to end of file + await this.moveTodo(file); + } + } + + private async moveTodo(targetFile: TFile, headingLineNumber?: number, nextHeadingLineNumber?: number): Promise { + const todoLine = this.todo.rawLine; + + // Remove from source file + await this.app.vault.process(this.sourceFile, (content) => { + return removeTodoLine(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); + }); + } else { + await this.app.vault.process(targetFile, (content) => { + return insertTodoAtEnd(content, todoLine); + }); + } + } +} diff --git a/src/settings.ts b/src/settings.ts index 352121e..e67fa2b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,5 @@ import {App, PluginSettingTab, Setting} from "obsidian"; -import MyPlugin from "./main"; +import TodoTrackerPlugin from "./main"; export interface MyPluginSettings { mySetting: string; @@ -10,9 +10,9 @@ export const DEFAULT_SETTINGS: MyPluginSettings = { } export class SampleSettingTab extends PluginSettingTab { - plugin: MyPlugin; + plugin: TodoTrackerPlugin; - constructor(app: App, plugin: MyPlugin) { + constructor(app: App, plugin: TodoTrackerPlugin) { super(app, plugin); this.plugin = plugin; } diff --git a/src/views/todo-item-component.ts b/src/views/todo-item-component.ts new file mode 100644 index 0000000..c08d362 --- /dev/null +++ b/src/views/todo-item-component.ts @@ -0,0 +1,55 @@ +import { Menu, setIcon } from 'obsidian'; +import type { TodoItem } from '../core/types'; + +export interface TodoItemCallbacks { + onToggle: () => void; + onMoveClick: () => void; +} + +/** + * Creates a DOM element for a todo item with checkbox and menu. + */ +export function createTodoItemEl( + container: HTMLElement, + todo: TodoItem, + callbacks: TodoItemCallbacks +): HTMLElement { + const itemEl = container.createEl('li', { cls: 'todo-tracker-item' }); + + // Checkbox + const checkboxEl = itemEl.createEl('input', { type: 'checkbox' }); + checkboxEl.checked = todo.completed; + checkboxEl.addEventListener('change', () => { + callbacks.onToggle(); + }); + + // Text content + const textEl = itemEl.createEl('span', { cls: 'todo-tracker-item-text' }); + textEl.setText(todo.text); + if (todo.completed) { + textEl.addClass('todo-tracker-item-completed'); + } + + // Menu button (ellipsis) + const menuBtn = itemEl.createEl('button', { cls: 'todo-tracker-menu-btn' }); + setIcon(menuBtn, 'more-vertical'); + menuBtn.setAttribute('aria-label', 'Todo options'); + + menuBtn.addEventListener('click', (evt) => { + evt.stopPropagation(); + + const menu = new Menu(); + menu.addItem((item) => { + item + .setTitle('Move to...') + .setIcon('arrow-right') + .onClick(() => { + callbacks.onMoveClick(); + }); + }); + + menu.showAtMouseEvent(evt); + }); + + return itemEl; +} diff --git a/src/views/todo-sidebar-view.ts b/src/views/todo-sidebar-view.ts new file mode 100644 index 0000000..1de5670 --- /dev/null +++ b/src/views/todo-sidebar-view.ts @@ -0,0 +1,117 @@ +import { ItemView, TFile, WorkspaceLeaf } from 'obsidian'; +import type { TodoItem } from '../core/types'; +import { parseTodos } from '../core/todo-parser'; +import { toggleTodo } from '../core/todo-transformer'; +import { createTodoItemEl } from './todo-item-component'; + +export const TODO_VIEW_TYPE = 'todo-tracker-view'; + +export class TodoSidebarView extends ItemView { + private currentFile: TFile | null = null; + + constructor(leaf: WorkspaceLeaf) { + super(leaf); + } + + getViewType(): string { + return TODO_VIEW_TYPE; + } + + getDisplayText(): string { + return 'Todo Tracker'; + } + + getIcon(): string { + return 'check-square'; + } + + async onOpen(): Promise { + await this.refresh(); + + // Listen for active leaf changes + this.registerEvent( + this.app.workspace.on('active-leaf-change', () => { + this.refresh(); + }) + ); + + // Listen for file modifications + this.registerEvent( + this.app.vault.on('modify', (file) => { + if (this.currentFile && file.path === this.currentFile.path) { + this.refresh(); + } + }) + ); + } + + async onClose(): Promise { + // Cleanup is handled automatically by registerEvent + } + + async refresh(): Promise { + const activeFile = this.app.workspace.getActiveFile(); + + // Check if it's a markdown file + if (!activeFile || activeFile.extension !== 'md') { + this.currentFile = null; + this.renderEmpty('Open a markdown file to see its todos'); + return; + } + + this.currentFile = activeFile; + + const content = await this.app.vault.read(activeFile); + const { todos } = parseTodos(content); + + if (todos.length === 0) { + this.renderEmpty('No todos in this note'); + return; + } + + this.renderTodos(todos, activeFile); + } + + private renderEmpty(message: string): void { + const container = this.contentEl; + container.empty(); + container.addClass('todo-tracker-container'); + + const emptyEl = container.createDiv({ cls: 'todo-tracker-empty' }); + emptyEl.setText(message); + } + + private renderTodos(todos: TodoItem[], file: TFile): void { + const container = this.contentEl; + container.empty(); + container.addClass('todo-tracker-container'); + + // Header with file name + const headerEl = container.createDiv({ cls: 'todo-tracker-header' }); + headerEl.setText(file.basename); + + // Todo list + const listEl = container.createEl('ul', { cls: 'todo-tracker-list' }); + + for (const todo of todos) { + const itemEl = createTodoItemEl(listEl, todo, { + onToggle: () => this.handleToggle(file, todo), + onMoveClick: () => this.handleMoveClick(todo, file), + }); + listEl.appendChild(itemEl); + } + } + + private async handleToggle(file: TFile, todo: TodoItem): Promise { + await this.app.vault.process(file, (content) => { + return toggleTodo(content, todo.lineNumber); + }); + } + + private handleMoveClick(todo: TodoItem, file: TFile): void { + // Import dynamically to avoid circular dependencies + import('../modals/note-select-modal').then(({ NoteSelectModal }) => { + new NoteSelectModal(this.app, todo, file).open(); + }); + } +} diff --git a/styles.css b/styles.css index 71cc60f..ff73b90 100644 --- a/styles.css +++ b/styles.css @@ -1,8 +1,77 @@ -/* +/* Todo Tracker Styles */ -This CSS file will be included with your plugin, and -available in the app when your plugin is enabled. +.todo-tracker-container { + padding: 10px; +} -If your plugin does not need CSS, delete this file. +.todo-tracker-header { + font-weight: 600; + font-size: 14px; + padding-bottom: 10px; + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 10px; + color: var(--text-muted); +} -*/ +.todo-tracker-empty { + color: var(--text-muted); + font-style: italic; + padding: 20px 0; + text-align: center; +} + +.todo-tracker-list { + list-style: none; + padding: 0; + margin: 0; +} + +.todo-tracker-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 4px; + border-radius: 4px; +} + +.todo-tracker-item:hover { + background-color: var(--background-modifier-hover); +} + +.todo-tracker-item input[type="checkbox"] { + flex-shrink: 0; + cursor: pointer; +} + +.todo-tracker-item-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.todo-tracker-item-completed { + text-decoration: line-through; + color: var(--text-muted); +} + +.todo-tracker-menu-btn { + flex-shrink: 0; + background: none; + border: none; + padding: 4px; + border-radius: 4px; + cursor: pointer; + color: var(--text-muted); + opacity: 0; + transition: opacity 0.15s ease; +} + +.todo-tracker-item:hover .todo-tracker-menu-btn { + opacity: 1; +} + +.todo-tracker-menu-btn:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..6973658 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + environment: 'node', + }, +});