diff --git a/.vscode/settings.json b/.vscode/settings.json index 573ab06..699e117 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "biome.lspBin": "./plugin/node_modules/@biomejs/biome/bin/biome", + "biome.lspBin": "/home/jamiebrynes/workspace/obsidian-todoist-plugin/plugin/node_modules/@biomejs/cli-linux-x64-musl/biome", "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, @@ -8,5 +8,6 @@ }, "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit" - } + }, + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 191213c..c503d6f 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -10,6 +10,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +> Note: the style changes in this release mean that you may need to tweak any custom CSS or themes. The changes are based on the default theme. + +### ✨ Features + +- Rebuilt the task renderer from the ground up. This now resembles Todoist more closely and has improved the default styling. + ## v1.13.0 (2024-04-10) ### ✨ Features diff --git a/nix/flake.nix b/nix/flake.nix index e8a239b..3ec9037 100644 --- a/nix/flake.nix +++ b/nix/flake.nix @@ -32,7 +32,6 @@ jq nodejs nodePackages.typescript-language-server - nodePackages.svelte-language-server marksman ]; enterShell = enterShellBySystem.${system} or ""; diff --git a/plugin/package-lock.json b/plugin/package-lock.json index d27b4ef..4aebc74 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -12,6 +12,7 @@ "@internationalized/date": "^3.5.2", "camelize-ts": "^3.0.0", "classnames": "^2.5.1", + "framer-motion": "^11.1.8", "moment": "^2.29.4", "obsidian": "0.15", "react": "^18.2.0", @@ -19,25 +20,20 @@ "react-dom": "^18.2.0", "react-textarea-autosize": "^8.5.3", "snakify-ts": "^2.3.0", - "svelte": "^4.2.10", - "svelte-select": "^5.0.1", "tslib": "^2.4.1", - "yaml": "^2.1.3" + "yaml": "^2.1.3", + "zustand": "^4.5.2" }, "devDependencies": { "@biomejs/biome": "^1.5.3", "@rollup/plugin-replace": "^5.0.5", - "@sveltejs/vite-plugin-svelte": "^3.0.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", - "@tsconfig/svelte": "^5.0.0", "@types/node": "^18.11.17", "@types/react-dom": "^18.2.19", "jsdom": "^24.0.0", "prettier": "^2.8.1", - "prettier-plugin-svelte": "^2.9.0", "sass": "^1.71.1", - "svelte-check": "^3.0.1", "typescript": "^5.3.3", "vite": "^5.1.0", "vite-plugin-static-copy": "^1.0.1", @@ -51,18 +47,6 @@ "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", "dev": true }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -406,32 +390,6 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -800,28 +758,6 @@ "node": ">=12" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", - "dependencies": { - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", - "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" - }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", @@ -911,48 +847,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -2632,45 +2531,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", - "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", - "dev": true, - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "svelte-hmr": "^0.15.3", - "vitefu": "^0.2.5" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", - "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.0.0 || >=20" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.0" - } - }, "node_modules/@swc/helpers": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", @@ -2821,44 +2681,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/svelte": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.2.tgz", - "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==", - "dev": true - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2891,19 +2713,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true - }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.61", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.61.tgz", "integrity": "sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2923,7 +2739,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@types/tern": { "version": "0.23.9", @@ -3033,6 +2849,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3098,18 +2915,11 @@ "node": ">= 8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, "dependencies": { "dequal": "^2.0.3" } @@ -3160,20 +2970,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3195,15 +2991,6 @@ "node": ">=8" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -3232,15 +3019,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/camelize-ts": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelize-ts/-/camelize-ts-3.0.0.tgz", @@ -3340,18 +3118,6 @@ "node": ">=6" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3382,20 +3148,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3432,7 +3184,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/data-urls": { "version": "5.0.0", @@ -3520,15 +3272,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3576,17 +3319,9 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=6" } }, "node_modules/diff-sequences": { @@ -3657,12 +3392,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, "node_modules/esbuild": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", @@ -3714,6 +3443,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -3801,6 +3531,30 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.1.8.tgz", + "integrity": "sha512-W2OGZmNfUarhh6A/rLXernq/JthjekbgeRWqzigPpbaShe/+HfQKUDSjiEdL302XOlINtO+SCFCiR1hlqN3uOA==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -3815,12 +3569,6 @@ "node": ">=14.14" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4075,31 +3823,6 @@ "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -4109,22 +3832,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4488,15 +4195,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/legacy-swc-helpers": { "name": "@swc/helpers", "version": "0.4.14", @@ -4506,17 +4204,6 @@ "tslib": "^2.4.0" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -4533,11 +4220,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4582,6 +4264,7 @@ "version": "0.30.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -4589,14 +4272,6 @@ "node": ">=12" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4667,27 +4342,6 @@ "node": ">=4" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mlly": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", @@ -4708,15 +4362,6 @@ "node": "*" } }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -4850,15 +4495,6 @@ "node": "*" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -4874,18 +4510,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -4898,15 +4522,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4931,24 +4546,6 @@ "node": "*" } }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/periscopic/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5015,48 +4612,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -5072,16 +4627,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-plugin-svelte": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.1.tgz", - "integrity": "sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==", - "dev": true, - "peerDependencies": { - "prettier": "^1.16.4 || ^2.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0" - } - }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -5369,60 +4914,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", @@ -5452,36 +4943,12 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, "node_modules/sass": { "version": "1.71.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", @@ -5619,25 +5086,11 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, "node_modules/source-map-js": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5720,169 +5173,6 @@ "node": ">=8" } }, - "node_modules/svelte": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.10.tgz", - "integrity": "sha512-Ep06yCaCdgG1Mafb/Rx8sJ1QS3RW2I2BxGp2Ui9LBHSZ2/tO/aGLc5WqPjgiAP6KAnLJGaIr/zzwQlOo1b8MxA==", - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte-check": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz", - "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", - "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.1.0", - "typescript": "^5.0.3" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" - } - }, - "node_modules/svelte-floating-ui": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/svelte-floating-ui/-/svelte-floating-ui-1.5.8.tgz", - "integrity": "sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==", - "dependencies": { - "@floating-ui/core": "^1.5.0", - "@floating-ui/dom": "^1.5.3" - } - }, - "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/svelte-preprocess": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", - "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.30.5", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 16.0.0", - "pnpm": "^8.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/svelte-select": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/svelte-select/-/svelte-select-5.8.3.tgz", - "integrity": "sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA==", - "dependencies": { - "svelte-floating-ui": "1.5.8" - } - }, - "node_modules/svelte/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/svelte/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/svelte/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5961,62 +5251,6 @@ "node": ">=18" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/tsconfck": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", @@ -6140,14 +5374,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/vite": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", @@ -6294,20 +5520,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vitest": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", @@ -6516,12 +5728,6 @@ "node": ">=8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/ws": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", @@ -6566,15 +5772,31 @@ "node": ">= 14" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "optional": true, - "peer": true, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, "engines": { - "node": ">=6" + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } } } } diff --git a/plugin/package.json b/plugin/package.json index ff61c88..4aeca4a 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -3,7 +3,7 @@ "version": "1.13.0", "description": "A Todoist plugin for Obsidian", "scripts": { - "check": "svelte-check", + "check": "tsc --noEmit", "dev": "npm run check && VITE_ENV=dev vite build", "build": "vite build", "test": "vitest", @@ -16,6 +16,7 @@ "@internationalized/date": "^3.5.2", "camelize-ts": "^3.0.0", "classnames": "^2.5.1", + "framer-motion": "^11.1.8", "moment": "^2.29.4", "obsidian": "0.15", "react": "^18.2.0", @@ -23,29 +24,24 @@ "react-dom": "^18.2.0", "react-textarea-autosize": "^8.5.3", "snakify-ts": "^2.3.0", - "svelte": "^4.2.10", - "svelte-select": "^5.0.1", "tslib": "^2.4.1", - "yaml": "^2.1.3" + "yaml": "^2.1.3", + "zustand": "^4.5.2" }, "devDependencies": { "@biomejs/biome": "^1.5.3", "@rollup/plugin-replace": "^5.0.5", - "@sveltejs/vite-plugin-svelte": "^3.0.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", - "@tsconfig/svelte": "^5.0.0", "@types/node": "^18.11.17", "@types/react-dom": "^18.2.19", "jsdom": "^24.0.0", "prettier": "^2.8.1", - "prettier-plugin-svelte": "^2.9.0", "sass": "^1.71.1", - "svelte-check": "^3.0.1", "typescript": "^5.3.3", "vite": "^5.1.0", "vite-plugin-static-copy": "^1.0.1", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.2.2" } -} +} \ No newline at end of file diff --git a/plugin/src/api/domain/project.ts b/plugin/src/api/domain/project.ts index ae89387..e5a5628 100644 --- a/plugin/src/api/domain/project.ts +++ b/plugin/src/api/domain/project.ts @@ -6,4 +6,5 @@ export type Project = { name: string; order: number; isInboxProject: boolean; + color: string; }; diff --git a/plugin/src/commands/addTask.ts b/plugin/src/commands/addTask.ts index a55e530..2880e04 100644 --- a/plugin/src/commands/addTask.ts +++ b/plugin/src/commands/addTask.ts @@ -28,11 +28,6 @@ export const addTaskWithPageInDescription: MakeCommand = (plugin: TodoistPlugin) const makeCallback = (plugin: TodoistPlugin, opts?: Partial) => { return () => { - if (plugin.options === null) { - new Notice("Failed to load settings, cannot open task creation modal."); - return; - } - plugin.services.modals.taskCreation({ initialContent: grabSelection(plugin), fileContext: getFileContext(plugin), diff --git a/plugin/src/components/CollapseIndicator.svelte b/plugin/src/components/CollapseIndicator.svelte deleted file mode 100644 index 271a93e..0000000 --- a/plugin/src/components/CollapseIndicator.svelte +++ /dev/null @@ -1,12 +0,0 @@ -
- -
- -
-
diff --git a/plugin/src/components/MarkdownRenderer.svelte b/plugin/src/components/MarkdownRenderer.svelte deleted file mode 100644 index 7d1b40f..0000000 --- a/plugin/src/components/MarkdownRenderer.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -
diff --git a/plugin/src/components/ObsidianIcon.svelte b/plugin/src/components/ObsidianIcon.svelte deleted file mode 100644 index d3757d1..0000000 --- a/plugin/src/components/ObsidianIcon.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
- - diff --git a/plugin/src/data/index.ts b/plugin/src/data/index.ts index 93dce4f..3dd70bb 100644 --- a/plugin/src/data/index.ts +++ b/plugin/src/data/index.ts @@ -15,11 +15,11 @@ export enum QueryErrorKind { Unknown = 3, } -type SubscriptionResult = +export type SubscriptionResult = | { type: "success"; tasks: Task[] } | { type: "error"; kind: QueryErrorKind }; -type OnSubscriptionChange = (result: SubscriptionResult) => void; -type Refresh = () => Promise; +export type OnSubscriptionChange = (result: SubscriptionResult) => void; +export type Refresh = () => Promise; type DataAccessor = { projects: RepositoryReader; @@ -27,15 +27,10 @@ type DataAccessor = { labels: RepositoryReader; }; -type Actions = { - closeTask: (id: TaskId) => Promise; - createTask: (content: string, params: CreateTaskParams) => Promise; -}; - export class TodoistAdapter { - public actions: Actions = { - closeTask: async (id) => await this.api.withInner((api) => api.closeTask(id)), - createTask: async (content, params) => + public actions = { + closeTask: async (id: TaskId) => await this.closeTask(id), + createTask: async (content: string, params: CreateTaskParams) => await this.api.withInner((api) => api.createTask(content, params)), }; @@ -43,7 +38,9 @@ export class TodoistAdapter { private readonly projects: Repository; private readonly sections: Repository; private readonly labels: Repository; - private readonly subscriptions: SubscriptionManager; + private readonly subscriptions: SubscriptionManager; + + private readonly tasksPendingClose: TaskId[]; private hasSynced = false; @@ -51,7 +48,8 @@ export class TodoistAdapter { this.projects = new Repository(() => this.api.withInner((api) => api.getProjects())); this.sections = new Repository(() => this.api.withInner((api) => api.getSections())); this.labels = new Repository(() => this.api.withInner((api) => api.getLabels())); - this.subscriptions = new SubscriptionManager(); + this.subscriptions = new SubscriptionManager(); + this.tasksPendingClose = []; } public isReady(): boolean { @@ -72,8 +70,8 @@ export class TodoistAdapter { await this.sections.sync(); await this.labels.sync(); - for (const refresh of this.subscriptions.listActive()) { - await refresh(); + for (const subscription of this.subscriptions.list()) { + await subscription.update(); } this.hasSynced = true; @@ -88,41 +86,19 @@ export class TodoistAdapter { } public subscribe(query: string, callback: OnSubscriptionChange): [UnsubscribeCallback, Refresh] { - const refresh = this.buildRefresher(query, callback); - return [this.subscriptions.subscribe(refresh), refresh]; + const fetcher = this.buildQueryFetcher(query); + const subscription = new Subscription(callback, fetcher, () => true); + return [this.subscriptions.subscribe(subscription), subscription.update]; } - private buildRefresher(query: string, callback: OnSubscriptionChange): Refresh { + private buildQueryFetcher(query: string): SubscriptionFetcher { return async () => { if (!this.api.hasValue()) { - return; - } - try { - const data = await this.api.withInner((api) => api.getTasks(query)); - const hydrated = data.map((t) => this.hydrate(t)); - callback({ type: "success", tasks: hydrated }); - } catch (error: unknown) { - console.error(`Failed to refresh task query: ${error}`); - - const result: SubscriptionResult = { - type: "error", - kind: QueryErrorKind.Unknown, - }; - if (error instanceof TodoistApiError) { - switch (error.statusCode) { - case 400: - result.kind = QueryErrorKind.BadRequest; - break; - case 401: - result.kind = QueryErrorKind.Unauthorized; - break; - case 403: - result.kind = QueryErrorKind.Forbidden; - break; - } - } - callback(result); + return []; } + const data = await this.api.withInner((api) => api.getTasks(query)); + const hydrated = data.map((t) => this.hydrate(t)); + return hydrated; }; } @@ -150,6 +126,31 @@ export class TodoistAdapter { order: apiTask.order, }; } + + private async closeTask(id: TaskId): Promise { + this.tasksPendingClose.push(id); + + for (const subscription of this.subscriptions.list()) { + subscription.callback(); + } + + try { + this.api.withInner((api) => api.closeTask(id)); + this.tasksPendingClose.remove(id); + + for (const subscription of this.subscriptions.list()) { + subscription.remove(id); + } + } catch (error: unknown) { + this.tasksPendingClose.remove(id); + + for (const subscription of this.subscriptions.list()) { + subscription.callback(); + } + + throw error; + } + } } const makeUnknownProject = (id: string): Project => { @@ -159,6 +160,7 @@ const makeUnknownProject = (id: string): Project => { name: "Unknown Project", order: Number.MAX_SAFE_INTEGER, isInboxProject: false, + color: "grey", }; }; @@ -170,3 +172,72 @@ const makeUnknownSection = (id: string): Section => { order: Number.MAX_SAFE_INTEGER, }; }; + +type SubscriptionFetcher = () => Promise; + +class Subscription { + private readonly userCallback: OnSubscriptionChange; + private readonly fetch: SubscriptionFetcher; + private readonly filter: () => boolean; + + private result: SubscriptionResult = { type: "success", tasks: [] }; + + constructor( + userCallback: OnSubscriptionChange, + fetch: SubscriptionFetcher, + filter: () => boolean, + ) { + this.userCallback = userCallback; + this.fetch = fetch; + this.filter = filter; + } + + public update = async () => { + try { + const data = await this.fetch(); + this.result = { type: "success", tasks: data }; + } catch (error: unknown) { + console.error(`Failed to refresh task query: ${error}`); + + const result: SubscriptionResult = { + type: "error", + kind: QueryErrorKind.Unknown, + }; + if (error instanceof TodoistApiError) { + switch (error.statusCode) { + case 400: + result.kind = QueryErrorKind.BadRequest; + break; + case 401: + result.kind = QueryErrorKind.Unauthorized; + break; + case 403: + result.kind = QueryErrorKind.Forbidden; + break; + } + } + + this.result = result; + } + + this.callback(); + }; + + public callback = () => { + // Apply filtering, without mutating the actual state of the result. + const result = { ...this.result }; + if (result.type === "success") { + result.tasks = result.tasks.filter(this.filter); + } + this.userCallback(result); + }; + + public remove(id: TaskId) { + if (this.result.type !== "success") { + return; + } + + this.result.tasks = this.result.tasks.filter((task) => task.id !== id); + this.callback(); + } +} diff --git a/plugin/src/data/subscriptions.ts b/plugin/src/data/subscriptions.ts index e028fda..d80e4fc 100644 --- a/plugin/src/data/subscriptions.ts +++ b/plugin/src/data/subscriptions.ts @@ -12,7 +12,7 @@ export class SubscriptionManager { return () => this.subscriptions.delete(id); } - public listActive(): IterableIterator { + public list(): IterableIterator { return this.subscriptions.values(); } } diff --git a/plugin/src/data/transformations/grouping.test.ts b/plugin/src/data/transformations/grouping.test.ts index 9f3b1e5..b031648 100644 --- a/plugin/src/data/transformations/grouping.test.ts +++ b/plugin/src/data/transformations/grouping.test.ts @@ -37,6 +37,7 @@ function makeProject(id: string, opts?: Partial): Project { name: opts?.name ?? "Project", order: opts?.order ?? 1, isInboxProject: false, + color: "grey", }; } diff --git a/plugin/src/data/transformations/relationships.test.ts b/plugin/src/data/transformations/relationships.test.ts index 24443cd..efbb1e4 100644 --- a/plugin/src/data/transformations/relationships.test.ts +++ b/plugin/src/data/transformations/relationships.test.ts @@ -19,6 +19,7 @@ function makeTask(id: string, opts?: Partial): Task { order: 1, parentId: null, isInboxProject: false, + color: "grey", }, section: opts?.section, diff --git a/plugin/src/data/transformations/sorting.test.ts b/plugin/src/data/transformations/sorting.test.ts index df3faf8..33b9102 100644 --- a/plugin/src/data/transformations/sorting.test.ts +++ b/plugin/src/data/transformations/sorting.test.ts @@ -20,6 +20,7 @@ function makeTask(id: string, opts?: Partial): Task { order: 1, parentId: null, isInboxProject: false, + color: "grey", }, section: opts?.section, diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 5568461..bd7fc9d 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -4,31 +4,17 @@ import "../styles.css"; import { TodoistApiClient } from "./api"; import { ObsidianFetcher } from "./api/fetcher"; import { registerCommands } from "./commands"; -import debug from "./log"; import { QueryInjector } from "./query/injector"; import { type Services, makeServices } from "./services"; -import { settings } from "./settings"; -import { type ISettings, defaultSettings } from "./settings"; +import { type Settings, useSettingsStore } from "./settings"; import { SettingsTab } from "./ui/settings"; export default class TodoistPlugin extends Plugin { - public options: ISettings; - public readonly services: Services; constructor(app: App, pluginManifest: PluginManifest) { super(app, pluginManifest); - this.options = { ...defaultSettings }; this.services = makeServices(this); - - settings.subscribe((value) => { - debug({ - msg: "Settings changed", - context: value, - }); - - this.options = value; - }); } async onload() { @@ -65,21 +51,18 @@ export default class TodoistPlugin extends Plugin { async loadOptions(): Promise { const options = await this.loadData(); - settings.update((old) => { + useSettingsStore.setState((old) => { return { ...old, - ...(options || {}), + ...options, }; - }); + }, true); - await this.saveData(this.options); + await this.saveData(useSettingsStore.getState()); } - async writeOptions(changeOpts: (settings: ISettings) => void): Promise { - settings.update((old) => { - changeOpts(old); - return old; - }); - await this.saveData(this.options); + async writeOptions(update: Partial): Promise { + useSettingsStore.setState(update); + await this.saveData(useSettingsStore.getState()); } } diff --git a/plugin/src/log.ts b/plugin/src/log.ts index f1e25c7..3112046 100644 --- a/plugin/src/log.ts +++ b/plugin/src/log.ts @@ -1,13 +1,7 @@ -import type { ISettings } from "./settings"; -import { settings } from "./settings"; - -let _settings: ISettings | undefined = undefined; -settings.subscribe((update) => { - _settings = update; -}); +import { useSettingsStore } from "@/settings"; export default function debug(log: string | LogMessage) { - if (!_settings?.debugLogging ?? false) { + if (!useSettingsStore.getState().debugLogging) { return; } diff --git a/plugin/src/query/injector.ts b/plugin/src/query/injector.ts deleted file mode 100644 index 4557570..0000000 --- a/plugin/src/query/injector.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { MarkdownRenderChild } from "obsidian"; -import type { MarkdownPostProcessorContext } from "obsidian"; -import type { SvelteComponent } from "svelte"; -import type TodoistPlugin from ".."; -import debug from "../log"; -import ErrorDisplay from "../ui/ErrorDisplay.svelte"; -import TodoistQuery from "../ui/TodoistQuery.svelte"; -import { parseQuery } from "./parser"; -import { applyReplacements } from "./replacements"; - -export class QueryInjector { - private readonly plugin: TodoistPlugin; - constructor(plugin: TodoistPlugin) { - this.plugin = plugin; - } - - onNewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { - let child: InjectedQuery; - - try { - const [query, warnings] = parseQuery(source); - applyReplacements(query, ctx); - - debug({ - msg: "Parsed query", - context: query, - }); - - // TODO: wire in component from Context - - child = new InjectedQuery(el, (root: HTMLElement) => { - return new TodoistQuery({ - target: root, - props: { - query: query, - warnings: warnings, - plugin: this.plugin, - }, - }); - }); - } catch (e) { - console.error(e); - child = new InjectedQuery(el, (root: HTMLElement) => { - return new ErrorDisplay({ target: root, props: { error: e as string } }); - }); - } - - ctx.addChild(child); - } -} - -class InjectedQuery extends MarkdownRenderChild { - private readonly createComp: (root: HTMLElement) => SvelteComponent; - private component: SvelteComponent | undefined = undefined; - - constructor(container: HTMLElement, createComp: (root: HTMLElement) => SvelteComponent) { - super(container); - this.createComp = createComp; - this.containerEl = container; - } - - onload() { - this.component = this.createComp(this.containerEl); - } - - onunload() { - if (this.component !== undefined) { - this.component.$destroy(); - } - } -} diff --git a/plugin/src/query/injector.tsx b/plugin/src/query/injector.tsx new file mode 100644 index 0000000..422d417 --- /dev/null +++ b/plugin/src/query/injector.tsx @@ -0,0 +1,113 @@ +import type TodoistPlugin from "@/index"; +import debug from "@/log"; +import { parseQuery } from "@/query/parser"; +import { applyReplacements } from "@/query/replacements"; +import { + type MarkdownEditButton, + MarkdownEditButtonContext, + PluginContext, + RenderChildContext, +} from "@/ui/context"; +import { QueryError } from "@/ui/query/QueryError"; +import { QueryRoot } from "@/ui/query/QueryRoot"; +import { MarkdownRenderChild } from "obsidian"; +import type { MarkdownPostProcessorContext } from "obsidian"; +import React from "react"; +import { type Root, createRoot } from "react-dom/client"; +import { type StoreApi, type UseBoundStore, create } from "zustand"; + +export class QueryInjector { + private readonly plugin: TodoistPlugin; + constructor(plugin: TodoistPlugin) { + this.plugin = plugin; + } + + onNewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { + let child: MarkdownRenderChild; + + try { + const [query, warnings] = parseQuery(source); + applyReplacements(query, ctx); + + debug({ + msg: "Parsed query", + context: query, + }); + + child = new ReactRenderer(el, this.plugin, QueryRoot, { query, warnings }, true); + } catch (e) { + console.error(e); + child = new ReactRenderer(el, this.plugin, QueryError, { error: e }, false); + } + + ctx.addChild(child); + } +} + +class ReactRenderer extends MarkdownRenderChild { + private readonly plugin: TodoistPlugin; + private readonly props: T; + private readonly component: React.FC; + private readonly reactRoot: Root; + + private readonly observer: MutationObserver; + + private readonly store: UseBoundStore>; + + constructor( + container: HTMLElement, + plugin: TodoistPlugin, + component: React.FC, + props: T, + interceptEditButton: boolean, + ) { + super(container); + this.plugin = plugin; + this.component = component; + this.props = props; + this.reactRoot = createRoot(this.containerEl); + this.store = create(() => { + return { click: () => {} }; + }); + + this.observer = new MutationObserver((mutations) => { + if (!interceptEditButton) { + return; + } + + for (const mutation of mutations) { + for (const addedNode of mutation.addedNodes) { + if (addedNode instanceof HTMLElement) { + if (addedNode.classList.contains("edit-block-button")) { + addedNode.hide(); + this.store.setState({ click: () => addedNode.click() }); + return; + } + } + } + } + }); + } + + onload(): void { + if (this.containerEl.parentElement !== null) { + this.observer.observe(this.containerEl.parentElement, { childList: true }); + } + + const Component = this.component; + this.reactRoot.render( + + + + + + + , + ); + } + + onunload(): void { + this.reactRoot.unmount(); + this.observer.disconnect(); + } +} diff --git a/plugin/src/services/modals.tsx b/plugin/src/services/modals.tsx index 929fed2..408a86a 100644 --- a/plugin/src/services/modals.tsx +++ b/plugin/src/services/modals.tsx @@ -1,9 +1,8 @@ +import { ModalContext, type ModalInfo, PluginContext } from "@/ui/context"; import { Modal, Platform } from "obsidian"; import React from "react"; import { type Root, createRoot } from "react-dom/client"; import type TodoistPlugin from ".."; -import { ModalContext, type ModalInfo } from "../ui/context/modal"; -import { PluginContext } from "../ui/context/plugin"; import { CreateTaskModal } from "../ui/createTaskModal"; import { OnboardingModal } from "../ui/onboardingModal"; diff --git a/plugin/src/settings.ts b/plugin/src/settings.ts index 74f3805..484a030 100644 --- a/plugin/src/settings.ts +++ b/plugin/src/settings.ts @@ -1,6 +1,6 @@ -import { writable } from "svelte/store"; +import { create } from "zustand"; -export const defaultSettings: ISettings = { +const defaultSettings: Settings = { fadeToggle: true, autoRefreshToggle: false, @@ -22,9 +22,7 @@ export const defaultSettings: ISettings = { debugLogging: false, }; -export const settings = writable({ ...defaultSettings }); - -export interface ISettings { +export type Settings = { fadeToggle: boolean; autoRefreshToggle: boolean; autoRefreshInterval: number; @@ -43,4 +41,8 @@ export interface ISettings { shouldWrapLinksInParens: boolean; debugLogging: boolean; -} +}; + +export const useSettingsStore = create((set) => ({ + ...defaultSettings, +})); diff --git a/plugin/src/ui/DescriptionRenderer.svelte b/plugin/src/ui/DescriptionRenderer.svelte deleted file mode 100644 index f57f56f..0000000 --- a/plugin/src/ui/DescriptionRenderer.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
- {#if isExpandedContent} - - {:else} - {description.split("\n")[0]}... - {/if} -
diff --git a/plugin/src/ui/ErrorDisplay.svelte b/plugin/src/ui/ErrorDisplay.svelte deleted file mode 100644 index 09c685e..0000000 --- a/plugin/src/ui/ErrorDisplay.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -
-

Encountered error:

-

{error}

-
diff --git a/plugin/src/ui/GroupedTasks.svelte b/plugin/src/ui/GroupedTasks.svelte deleted file mode 100644 index 738e44f..0000000 --- a/plugin/src/ui/GroupedTasks.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -{#each grouped as group (group.header)} -
-
- {group.header} -
- -
-{/each} diff --git a/plugin/src/ui/NoTaskDisplay.svelte b/plugin/src/ui/NoTaskDisplay.svelte deleted file mode 100644 index a03da4b..0000000 --- a/plugin/src/ui/NoTaskDisplay.svelte +++ /dev/null @@ -1,3 +0,0 @@ -
-

Nothing to-do! Sit back and relax.

-
diff --git a/plugin/src/ui/QueryErrorDisplay.svelte b/plugin/src/ui/QueryErrorDisplay.svelte deleted file mode 100644 index 4b84800..0000000 --- a/plugin/src/ui/QueryErrorDisplay.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/plugin/src/ui/TaskList.svelte b/plugin/src/ui/TaskList.svelte deleted file mode 100644 index ddc5429..0000000 --- a/plugin/src/ui/TaskList.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -
    - {#each taskTrees as taskTree (taskTree.id)} - - {/each} -
diff --git a/plugin/src/ui/TaskListRoot.svelte b/plugin/src/ui/TaskListRoot.svelte deleted file mode 100644 index 5e53d6a..0000000 --- a/plugin/src/ui/TaskListRoot.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/plugin/src/ui/TaskRenderer.svelte b/plugin/src/ui/TaskRenderer.svelte deleted file mode 100644 index 1560694..0000000 --- a/plugin/src/ui/TaskRenderer.svelte +++ /dev/null @@ -1,195 +0,0 @@ - - -
  • -
    - { - await taskActions.close(taskTree.id); - }} - /> - -
    - {#if shouldRenderDescription} - - {/if} - - {#if taskTree.children.length != 0} - - {/if} -
  • diff --git a/plugin/src/ui/TodoistQuery.svelte b/plugin/src/ui/TodoistQuery.svelte deleted file mode 100644 index 8307d14..0000000 --- a/plugin/src/ui/TodoistQuery.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - -{#if title.length != 0} -

    {title}

    -{/if} -
    { - await forceRefresh(); - }} - aria-label="Refresh list" -> - -
    -
    { - callTaskModal(); - }} - aria-label="Add item" -> - -
    -
    -{#if warnings.length !== 0} -
    -
    - - Warning -
    -
    -
      - {#each warnings as warning} -
    • {warning}
    • - {/each} -
    -
    -
    -{/if} -{#if fetchedOnce} - {#if queryError !== undefined} - - {:else if filteredTasks.length === 0} - - {:else if query.groupBy !== GroupVariant.None} - - {:else} - - {/if} -{/if} diff --git a/plugin/src/ui/components/callout/index.tsx b/plugin/src/ui/components/callout/index.tsx new file mode 100644 index 0000000..6b03666 --- /dev/null +++ b/plugin/src/ui/components/callout/index.tsx @@ -0,0 +1,29 @@ +import { ObsidianIcon } from "@/ui/components/obsidian-icon"; +import classNames from "classnames"; +import React from "react"; +import "./styles.scss"; + +type Props = { + title: string; + className: string; + iconId: string; + contents?: string[]; +}; + +export const Callout: React.FC = ({ title, contents, iconId, className }) => { + return ( +
    +
    + + {title} +
    + {contents && ( +
      + {contents.map((content) => ( +
    • {content}
    • + ))} +
    + )} +
    + ); +}; diff --git a/plugin/src/ui/components/callout/styles.scss b/plugin/src/ui/components/callout/styles.scss new file mode 100644 index 0000000..75850df --- /dev/null +++ b/plugin/src/ui/components/callout/styles.scss @@ -0,0 +1,22 @@ +.todoist-callout { + margin-top: 1em; + padding: 16px; + background-color: var(--todoist-callout-color); + border-radius: 4px; + + .callout-header { + display: flex; + align-items: center; + margin-bottom: 4px; + font-weight: 600; + + .obsidian-icon { + margin-right: 0.5em; + } + } + + ul { + margin-block-start: 0em; + margin-block-end: 0em; + } +} diff --git a/plugin/src/ui/components/markdown/index.tsx b/plugin/src/ui/components/markdown/index.tsx new file mode 100644 index 0000000..bb17561 --- /dev/null +++ b/plugin/src/ui/components/markdown/index.tsx @@ -0,0 +1,40 @@ +import { RenderChildContext } from "@/ui/context"; +import { MarkdownRenderer } from "obsidian"; +import React, { useEffect, useRef } from "react"; + +interface MarkdownProps { + content: string; + className?: string; +} + +const Markdown: React.FC = ({ content, className }) => { + const renderChild = RenderChildContext.use(); + const ref = useRef(null); + + useEffect(() => { + const renderMarkdown = async () => { + if (ref.current === null) { + return; + } + + await MarkdownRenderer.renderMarkdown(content, ref.current, "", renderChild); + + if (ref.current.childElementCount > 1) { + return; + } + + const markdownContent = ref.current.querySelector("p"); + + if (markdownContent !== null) { + markdownContent.parentElement?.removeChild(markdownContent); + ref.current.innerHTML = markdownContent.innerHTML; + } + }; + + renderMarkdown(); + }, [content, renderChild]); + + return
    ; +}; + +export default Markdown; diff --git a/plugin/src/ui/components/obsidian-icon/index.tsx b/plugin/src/ui/components/obsidian-icon/index.tsx index 3255625..bc37883 100644 --- a/plugin/src/ui/components/obsidian-icon/index.tsx +++ b/plugin/src/ui/components/obsidian-icon/index.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { setIcon } from "obsidian"; import React, { useEffect } from "react"; import { useRef } from "react"; @@ -10,8 +11,7 @@ type Props = { }; export const ObsidianIcon: React.FC = ({ size, id, className }) => { - const div = useRef(null); - + const div = useRef(null); useEffect(() => { if (div.current === null) { return; @@ -20,5 +20,5 @@ export const ObsidianIcon: React.FC = ({ size, id, className }) => { setIcon(div.current, id, size); }, [id, size]); - return
    ; + return
    ; }; diff --git a/plugin/src/ui/context.ts b/plugin/src/ui/context.ts new file mode 100644 index 0000000..f7485b3 --- /dev/null +++ b/plugin/src/ui/context.ts @@ -0,0 +1,43 @@ +import type TodoistPlugin from "@/index"; +import type { Query } from "@/query/query"; +import type { MarkdownRenderChild } from "obsidian"; +import { type Provider, createContext, useContext } from "react"; +import type { StoreApi, UseBoundStore } from "zustand"; + +type Context = { + Provider: Provider; + use: () => T; +}; + +const makeContext = (): Context => { + const context = createContext(undefined); + const use = () => { + const ctx = useContext(context); + + if (ctx === undefined) { + throw new Error("Context provider not found"); + } + + return ctx; + }; + return { Provider: context.Provider, use }; +}; + +export const QueryContext = makeContext(); + +export const PluginContext = makeContext(); + +export type ModalInfo = { + close: () => void; + popoverContainerEl: HTMLElement; +}; + +export const ModalContext = makeContext(); + +export const RenderChildContext = makeContext(); + +export type MarkdownEditButton = { + click: () => void; +}; + +export const MarkdownEditButtonContext = makeContext>>(); diff --git a/plugin/src/ui/context/modal.ts b/plugin/src/ui/context/modal.ts deleted file mode 100644 index 04f6ef2..0000000 --- a/plugin/src/ui/context/modal.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createContext, useContext } from "react"; - -export type ModalInfo = { - close: () => void; - popoverContainerEl: HTMLElement; -}; - -export const ModalContext = createContext(undefined); - -export const useModalContext = () => { - const modal = useContext(ModalContext); - - if (modal === undefined) { - throw new Error("ModalContext provider not found"); - } - - return modal; -}; diff --git a/plugin/src/ui/context/plugin.ts b/plugin/src/ui/context/plugin.ts deleted file mode 100644 index 8aabcac..0000000 --- a/plugin/src/ui/context/plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createContext, useContext } from "react"; -import TodoistPlugin from "../.."; - -export const PluginContext = createContext(undefined); - -export const usePluginContext = () => { - const plugin = useContext(PluginContext); - - if (plugin === undefined) { - throw new Error("PluginContext provider not found"); - } - - return plugin; -}; diff --git a/plugin/src/ui/contexts.ts b/plugin/src/ui/contexts.ts deleted file mode 100644 index 63ec1a6..0000000 --- a/plugin/src/ui/contexts.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Component } from "obsidian"; -import { getContext, setContext } from "svelte"; -import type { TaskId } from "../api/domain/task"; -import type { Query } from "../query/query"; - -const taskActionsKey = "todoist-task-actions"; - -type TaskActions = { - close: (id: TaskId) => Promise; -}; - -export function setTaskActions(actions: TaskActions) { - setContext(taskActionsKey, actions); -} - -export function getTaskActions(): TaskActions { - return getContext(taskActionsKey); -} - -const queryKey = "todoist-query"; - -export function setQuery(query: Query) { - setContext(queryKey, query); -} - -export function getQuery(): Query { - return getContext(queryKey); -} - -const componentKey = "todoist-component"; - -export function setComponent(component: Component) { - setContext(componentKey, component); -} - -export function getComponent(): Component { - return getContext(componentKey); -} diff --git a/plugin/src/ui/createTaskModal/LabelSelector.tsx b/plugin/src/ui/createTaskModal/LabelSelector.tsx index d02ac1a..095cd10 100644 --- a/plugin/src/ui/createTaskModal/LabelSelector.tsx +++ b/plugin/src/ui/createTaskModal/LabelSelector.tsx @@ -1,9 +1,8 @@ -import classNames from "classnames"; +import { PluginContext } from "@/ui/context"; import React, { useMemo } from "react"; import { Button, DialogTrigger, ListBox, ListBoxItem, type Selection } from "react-aria-components"; import type { Label } from "../../api/domain/label"; import { ObsidianIcon } from "../components/obsidian-icon"; -import { usePluginContext } from "../context/plugin"; import { Popover } from "./Popover"; type Props = { @@ -12,7 +11,7 @@ type Props = { }; export const LabelSelector: React.FC = ({ selected, setSelected }) => { - const plugin = usePluginContext(); + const plugin = PluginContext.use(); const options = useMemo(() => { return Array.from(plugin.services.todoist.data().labels.iter()); diff --git a/plugin/src/ui/createTaskModal/Popover.tsx b/plugin/src/ui/createTaskModal/Popover.tsx index 10f5c82..996567e 100644 --- a/plugin/src/ui/createTaskModal/Popover.tsx +++ b/plugin/src/ui/createTaskModal/Popover.tsx @@ -1,8 +1,8 @@ +import { ModalContext } from "@/ui/context"; import { Platform } from "obsidian"; import type { PropsWithChildren } from "react"; import React from "react"; import { Popover as AriaPopover, type PopoverProps } from "react-aria-components"; -import { useModalContext } from "../context/modal"; type Props = { maxHeight?: number; @@ -14,7 +14,7 @@ export const Popover: React.FC> = ({ defaultPlacement, maxHeight, }) => { - const modal = useModalContext(); + const modal = ModalContext.use(); return ( = ({ selected, setSelected }) => { - const plugin = usePluginContext(); + const plugin = PluginContext.use(); const todoistData = plugin.services.todoist.data(); const [filter, setFilter] = useState(""); @@ -223,7 +223,7 @@ const ProjectLabel: React.FC<{ project: Project }> = ({ project }) => { }; const ButtonLabel: React.FC = ({ projectId, sectionId }) => { - const { projects, sections } = usePluginContext().services.todoist.data(); + const { projects, sections } = PluginContext.use().services.todoist.data(); const selectedProject = projects.byId(projectId); if (selectedProject === undefined) { diff --git a/plugin/src/ui/createTaskModal/index.tsx b/plugin/src/ui/createTaskModal/index.tsx index 41a2db3..b858be3 100644 --- a/plugin/src/ui/createTaskModal/index.tsx +++ b/plugin/src/ui/createTaskModal/index.tsx @@ -1,3 +1,5 @@ +import { useSettingsStore } from "@/settings"; +import { ModalContext, PluginContext } from "@/ui/context"; import { getLocalTimeZone, toCalendarDateTime, toZoned } from "@internationalized/date"; import { Notice, TFile } from "obsidian"; import React, { useEffect, useState } from "react"; @@ -5,8 +7,6 @@ import { Button } from "react-aria-components"; import type TodoistPlugin from "../.."; import type { Label } from "../../api/domain/label"; import type { CreateTaskParams, Priority } from "../../api/domain/task"; -import { useModalContext } from "../context/modal"; -import { usePluginContext } from "../context/plugin"; import { type DueDate, DueDateSelector } from "./DueDateSelector"; import { LabelSelector } from "./LabelSelector"; import { PrioritySelector } from "./PrioritySelector"; @@ -26,7 +26,7 @@ type CreateTaskProps = { }; export const CreateTaskModal: React.FC = (props) => { - const plugin = usePluginContext(); + const plugin = PluginContext.use(); const [isReady, setIsReady] = useState(plugin.services.todoist.isReady()); @@ -57,8 +57,9 @@ const CreateTaskModalContent: React.FC = ({ fileContext, options: initialOptions, }) => { - const plugin = usePluginContext(); - const modal = useModalContext(); + const plugin = PluginContext.use(); + const settings = useSettingsStore(); + const modal = ModalContext.use(); const [content, setContent] = useState(initialContent); const [description, setDescription] = useState(""); @@ -75,11 +76,11 @@ const CreateTaskModalContent: React.FC = ({ const builder = [initial]; if (withLink && fileContext !== undefined) { builder.push(" "); - if (plugin.options.shouldWrapLinksInParens) { + if (settings.shouldWrapLinksInParens) { builder.push("("); } builder.push(getLinkForFile(fileContext)); - if (plugin.options.shouldWrapLinksInParens) { + if (settings.shouldWrapLinksInParens) { builder.push(")"); } } diff --git a/plugin/src/ui/onboardingModal/index.tsx b/plugin/src/ui/onboardingModal/index.tsx index c9e5ab3..c8f230b 100644 --- a/plugin/src/ui/onboardingModal/index.tsx +++ b/plugin/src/ui/onboardingModal/index.tsx @@ -1,7 +1,7 @@ +import { ModalContext } from "@/ui/context"; import { Notice } from "obsidian"; import React from "react"; import { TokenValidation } from "../../token"; -import { useModalContext } from "../context/modal"; import { TokenInputForm } from "./TokenInputForm"; import "./styles.scss"; @@ -12,7 +12,7 @@ type OnboardingProps = { }; export const OnboardingModal: React.FC = ({ onTokenSubmit }) => { - const modal = useModalContext(); + const modal = ModalContext.use(); const callback = (token: string) => { modal.close(); diff --git a/plugin/src/ui/query/QueryError.tsx b/plugin/src/ui/query/QueryError.tsx new file mode 100644 index 0000000..ad75439 --- /dev/null +++ b/plugin/src/ui/query/QueryError.tsx @@ -0,0 +1,25 @@ +import { Callout } from "@/ui/components/callout"; +import React from "react"; + +type Props = { + error: unknown; +}; + +export const QueryError: React.FC = ({ error }) => { + return ( + + ); +}; + +const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message; + } + + return "Unknown error occurred. Please check the Console in the Developer Tools window for more information"; +}; diff --git a/plugin/src/ui/query/QueryHeader.tsx b/plugin/src/ui/query/QueryHeader.tsx new file mode 100644 index 0000000..67d82d0 --- /dev/null +++ b/plugin/src/ui/query/QueryHeader.tsx @@ -0,0 +1,66 @@ +import { fireCommand } from "@/commands"; +import { ObsidianIcon } from "@/ui/components/obsidian-icon"; +import { MarkdownEditButtonContext, PluginContext } from "@/ui/context"; +import classNames from "classnames"; +import React from "react"; +import { Button } from "react-aria-components"; + +type Props = { + title: string; + isFetching: boolean; + refresh: () => Promise; +}; + +export const QueryHeader: React.FC = ({ title, isFetching, refresh }) => { + const plugin = PluginContext.use(); + const { click: editBlock } = MarkdownEditButtonContext.use()(); + + return ( +
    + {title} +
    + fireCommand("add-task-page-content", plugin)} + /> + { + await refresh(); + }} + /> + { + editBlock(); + }} + /> +
    +
    + ); +}; + +type ButtonProps = { + iconId: string; + action: () => Promise | void; + className: string; +}; + +const HeaderButton: React.FC = ({ iconId, action, className }) => { + const handler = async () => { + const result = action(); + + if (result instanceof Promise) { + await result; + } + }; + + return ( + + ); +}; diff --git a/plugin/src/ui/query/QueryRoot.tsx b/plugin/src/ui/query/QueryRoot.tsx new file mode 100644 index 0000000..9216c21 --- /dev/null +++ b/plugin/src/ui/query/QueryRoot.tsx @@ -0,0 +1,137 @@ +import { type OnSubscriptionChange, type Refresh, type SubscriptionResult } from "@/data"; +import type TodoistPlugin from "@/index"; +import type { QueryWarning } from "@/query/parser"; +import { GroupVariant, type Query } from "@/query/query"; +import { type Settings, useSettingsStore } from "@/settings"; +import { PluginContext, QueryContext } from "@/ui/context"; +import { QueryHeader } from "@/ui/query/QueryHeader"; +import { QueryWarnings } from "@/ui/query/QueryWarnings"; +import { Displays } from "@/ui/query/displays"; +import React, { useCallback, useEffect, useState } from "react"; +import "./styles.scss"; + +const useSubscription = ( + plugin: TodoistPlugin, + query: Query, + callback: OnSubscriptionChange, +): [Refresh, boolean, boolean] => { + const [refresher, setRefresher] = useState(undefined); + const [isFetching, setIsFetching] = useState(false); + const [hasFetchedOnce, setHasFetchedOnce] = useState(false); + + useEffect(() => { + const [unsub, refresh] = plugin.services.todoist.subscribe(query.filter, callback); + setRefresher(() => { + return refresh; + }); + return unsub; + }, [query, plugin, callback]); + + const forceRefresh = useCallback(async () => { + if (refresher === undefined) { + return; + } + + setIsFetching(true); + await refresher(); + setHasFetchedOnce(true); + setIsFetching(false); + }, [refresher]); + + useEffect(() => { + forceRefresh(); + }, [forceRefresh]); + + return [forceRefresh, isFetching, hasFetchedOnce]; +}; + +type Props = { + query: Query; + warnings: QueryWarning[]; +}; + +export const QueryRoot: React.FC = ({ query, warnings }) => { + const plugin = PluginContext.use(); + const settings = useSettingsStore(); + const [result, setResult] = useState({ type: "success", tasks: [] }); + const [refresh, isFetching, hasFetchedOnce] = useSubscription(plugin, query, setResult); + + useEffect(() => { + const interval = getAutorefreshInterval(query, settings); + + if (interval === undefined) { + return; + } + + const id = window.setInterval(async () => { + await refresh(); + }, interval * 1000); + + return () => window.clearInterval(id); + }, [query, settings, refresh]); + + return ( + <> + + + {hasFetchedOnce && } + + ); +}; + +const getAutorefreshInterval = (query: Query, settings: Settings): number | undefined => { + if (query.autorefresh !== 0) { + return query.autorefresh; + } + + if (!settings.autoRefreshToggle) { + return undefined; + } + + if (settings.autoRefreshInterval !== 0) { + return settings.autoRefreshInterval; + } + + return undefined; +}; + +const getTitle = (query: Query, result: SubscriptionResult): string => { + if (query.name.length === 0) { + return ""; + } + + switch (result.type) { + case "error": + return `${query.name} (Error)`; + case "success": + return query.name.replace("{task_count}", result.tasks.length.toString()); + } +}; + +const QueryResponseHandler: React.FC<{ + result: SubscriptionResult; + query: Query; +}> = ({ result, query }) => { + if (result.type === "error") { + return ; + } + + const tasks = result.tasks; + if (tasks.length === 0) { + return ; + } + + if (query.groupBy !== GroupVariant.None) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/plugin/src/ui/query/QueryWarnings.tsx b/plugin/src/ui/query/QueryWarnings.tsx new file mode 100644 index 0000000..2a47433 --- /dev/null +++ b/plugin/src/ui/query/QueryWarnings.tsx @@ -0,0 +1,22 @@ +import type { QueryWarning } from "@/query/parser"; +import { Callout } from "@/ui/components/callout"; +import React from "react"; + +type Props = { + warnings: QueryWarning[]; +}; + +export const QueryWarnings: React.FC = ({ warnings }) => { + if (warnings.length === 0) { + return <>; + } + + return ( + + ); +}; diff --git a/plugin/src/ui/query/displays/EmptyDisplay.tsx b/plugin/src/ui/query/displays/EmptyDisplay.tsx new file mode 100644 index 0000000..23328e0 --- /dev/null +++ b/plugin/src/ui/query/displays/EmptyDisplay.tsx @@ -0,0 +1,6 @@ +import { Callout } from "@/ui/components/callout"; +import React from "react"; + +export const EmptyDisplay: React.FC = () => { + return ; +}; diff --git a/plugin/src/ui/query/displays/ErrorDisplay.tsx b/plugin/src/ui/query/displays/ErrorDisplay.tsx new file mode 100644 index 0000000..6da44d3 --- /dev/null +++ b/plugin/src/ui/query/displays/ErrorDisplay.tsx @@ -0,0 +1,31 @@ +import { QueryErrorKind } from "@/data"; +import { Callout } from "@/ui/components/callout"; +import React from "react"; + +const getErrorMessage = (kind: QueryErrorKind): string => { + switch (kind) { + case QueryErrorKind.BadRequest: + return "The Todoist API has rejected the request. Please check the filter to ensure it is valid."; + case QueryErrorKind.Unauthorized: + return "The Todoist API request is missing or has the incorrect credentials. Please check the API token in the settings."; + default: + return "Unknown error occurred. Please check the Console in the Developer Tools window for more information"; + } +}; + +type Props = { + kind: QueryErrorKind; +}; + +export const ErrorDisplay: React.FC = ({ kind }) => { + const errorMessage = getErrorMessage(kind); + + return ( + + ); +}; diff --git a/plugin/src/ui/query/displays/GroupedDisplay.tsx b/plugin/src/ui/query/displays/GroupedDisplay.tsx new file mode 100644 index 0000000..d07f7ca --- /dev/null +++ b/plugin/src/ui/query/displays/GroupedDisplay.tsx @@ -0,0 +1,25 @@ +import type { Task } from "@/data/task"; +import { groupBy } from "@/data/transformations/grouping"; +import { QueryContext } from "@/ui/context"; +import { ListDisplay } from "@/ui/query/displays/ListDisplay"; +import React from "react"; + +type Props = { + tasks: Task[]; +}; + +export const GroupedDisplay: React.FC = ({ tasks }) => { + const query = QueryContext.use(); + const groups = groupBy(tasks, query.groupBy); + + return ( + <> + {groups.map((group) => ( +
    +
    {group.header}
    + +
    + ))} + + ); +}; diff --git a/plugin/src/ui/query/displays/ListDisplay.tsx b/plugin/src/ui/query/displays/ListDisplay.tsx new file mode 100644 index 0000000..14bf32e --- /dev/null +++ b/plugin/src/ui/query/displays/ListDisplay.tsx @@ -0,0 +1,24 @@ +import type { Task } from "@/data/task"; +import { type TaskTree, buildTaskTree } from "@/data/transformations/relationships"; +import { sortTasks } from "@/data/transformations/sorting"; +import type { SortingVariant } from "@/query/query"; +import { QueryContext } from "@/ui/context"; +import { TaskList } from "@/ui/query/task/TaskList"; +import React from "react"; + +type Props = { + tasks: Task[]; +}; + +export const ListDisplay: React.FC = ({ tasks }) => { + const query = QueryContext.use(); + const trees = getTaskTree(tasks, query.sorting); + + return ; +}; + +const getTaskTree = (tasks: Task[], sorting: SortingVariant[]): TaskTree[] => { + const copy = [...tasks]; + sortTasks(copy, sorting); + return buildTaskTree(copy); +}; diff --git a/plugin/src/ui/query/displays/index.ts b/plugin/src/ui/query/displays/index.ts new file mode 100644 index 0000000..fc94463 --- /dev/null +++ b/plugin/src/ui/query/displays/index.ts @@ -0,0 +1,11 @@ +import { EmptyDisplay } from "@/ui/query/displays/EmptyDisplay"; +import { ErrorDisplay } from "@/ui/query/displays/ErrorDisplay"; +import { GroupedDisplay } from "@/ui/query/displays/GroupedDisplay"; +import { ListDisplay } from "@/ui/query/displays/ListDisplay"; + +export const Displays = { + Error: ErrorDisplay, + Empty: EmptyDisplay, + List: ListDisplay, + Grouped: GroupedDisplay, +}; diff --git a/plugin/src/ui/query/styles.scss b/plugin/src/ui/query/styles.scss new file mode 100644 index 0000000..64a7117 --- /dev/null +++ b/plugin/src/ui/query/styles.scss @@ -0,0 +1,258 @@ +.todoist-query-header { + display: flex; + align-items: center; + justify-content: space-between; + + .todoist-query-title { + font-size: 1.25em; + } + + .todoist-query-controls { + display: flex; + align-items: center; + justify-content: end; + + * + * { + margin-left: 0.5em; + } + + .todoist-query-control-button { + padding: var(--size-2-2) var(--size-2-3); + color: var(--text-muted); + border-radius: var(--radius-s); + border: 1px solid var(--color-base-40); + box-shadow: none; + + transition: opacity 0.33s; + opacity: 0; + + .block-language-todoist:hover & { + opacity: 1; + } + + &:hover { + background-color: inherit; + border: 1px solid var(--interactive-accent); + } + + .markdown-reading-view &.edit-query { + display: none; + } + + &.refresh-query.is-refreshing > .obsidian-icon { + @-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } + } + + animation: spin 1s linear infinite reverse; + } + } + } +} + +.todoist-query-warnings { + --todoist-callout-color: rgba(var(--color-yellow-rgb), 0.2); +} + +.todoist-tasks-list { + margin-top: 1em; + + .todoist-tasks-list { + margin-top: 0em; + margin-left: 2em; + + & + .todoist-task-container { + border-top: none; + } + + .todoist-task-container:first-child { + border-top: none; + } + } +} + +.todoist-task-container { + display: flex; + padding: 0.5em 0; + border-bottom: 1px solid var(--todoist-task-separator-color); + + &:first-child { + border-top: 1px solid var(--todoist-task-separator-color); + } + + &[data-priority="1"] { + --todoist-checkbox-border: var(--todoist-p4-border); + --todoist-checkbox-border-hover: var(--todoist-p4-border-hover); + --todoist-checkbox-background: var(--todoist-p4-background); + } + &[data-priority="2"] { + --todoist-checkbox-border: var(--todoist-p3-border); + --todoist-checkbox-border-hover: var(--todoist-p3-border-hover); + --todoist-checkbox-background: var(--todoist-p3-background); + } + &[data-priority="3"] { + --todoist-checkbox-border: var(--todoist-p2-border); + --todoist-checkbox-border-hover: var(--todoist-p2-border-hover); + --todoist-checkbox-background: var(--todoist-p2-background); + } + &[data-priority="4"] { + --todoist-checkbox-border: var(--todoist-p1-border); + --todoist-checkbox-border-hover: var(--todoist-p1-border-hover); + --todoist-checkbox-background: var(--todoist-p1-background); + } + + .todoist-task-checkbox { + margin-top: 3px; + + div { + height: 16px; + width: 16px; + border-radius: 50%; + border: 1px solid var(--todoist-checkbox-border); + background-color: var(--todoist-checkbox-background); + + &:hover { + border: 1px solid var(--todoist-checkbox-border-hover); + } + } + } + + .todoist-task { + margin-left: 0.5em; + width: 100%; + + .todoist-task-description { + font-size: var(--font-small); + color: var(--text-muted); + } + + .todoist-task-metadata { + display: flex; + justify-content: space-between; + font-size: var(--font-smaller); + margin-top: 0.25em; + --icon-size: 14px; + color: var(--text-muted); + + & > div { + display: flex; + + & > * + * { + margin-left: 0.5em; + } + } + + .task-metadata-item { + display: flex; + align-items: center; + + & > * + * { + margin-left: 0.25em; + } + + &[data-task-metadata-kind="project"] .obsidian-icon { + color: var(--todoist-project-color); + } + } + } + } + + &[data-project-color="berry_red"] { + --todoist-project-color: var(--todoist-berry-red); + } + + &[data-project-color="red"] { + --todoist-project-color: var(--todoist-red); + } + + &[data-project-color="orange"] { + --todoist-project-color: var(--todoist-orange); + } + + &[data-project-color="yellow"] { + --todoist-project-color: var(--todoist-yellow); + } + + &[data-project-color="olive_green"] { + --todoist-project-color: var(--todoist-olive-green); + } + + &[data-project-color="lime_green"] { + --todoist-project-color: var(--todoist-lime-green); + } + + &[data-project-color="green"] { + --todoist-project-color: var(--todoist-green); + } + + &[data-project-color="mint_green"] { + --todoist-project-color: var(--todoist-mint-green); + } + + &[data-project-color="teal"] { + --todoist-project-color: var(--todoist-teal); + } + + &[data-project-color="sky_blue"] { + --todoist-project-color: var(--todoist-sky-blue); + } + + &[data-project-color="light_blue"] { + --todoist-project-color: var(--todoist-light-blue); + } + + &[data-project-color="blue"] { + --todoist-project-color: var(--todoist-blue); + } + + &[data-project-color="grape"] { + --todoist-project-color: var(--todoist-grape); + } + + &[data-project-color="violet"] { + --todoist-project-color: var(--todoist-violet); + } + + &[data-project-color="lavender"] { + --todoist-project-color: var(--todoist-lavender); + } + + &[data-project-color="magenta"] { + --todoist-project-color: var(--todoist-magenta); + } + + &[data-project-color="salmon"] { + --todoist-project-color: var(--todoist-salmon); + } + + &[data-project-color="charcoal"] { + --todoist-project-color: var(--todoist-charcoal); + } + + &[data-project-color="grey"] { + --todoist-project-color: var(--todoist-grey); + } + + &[data-project-color="taupe"] { + --todoist-project-color: var(--todoist-taupe); + } +} + +.todoist-group + .todoist-group { + margin-top: 2em; +} + +.todoist-group-title { + margin: 1em 0; + font-weight: 600; +} + +.todoist-no-tasks { + --todoist-callout-color: rgba(var(--color-green-rgb), 0.2); +} + +.todoist-query-error { + --todoist-callout-color: rgba(var(--color-red-rgb), 0.2); +} \ No newline at end of file diff --git a/plugin/src/ui/query/task/Task.tsx b/plugin/src/ui/query/task/Task.tsx new file mode 100644 index 0000000..18dcab5 --- /dev/null +++ b/plugin/src/ui/query/task/Task.tsx @@ -0,0 +1,148 @@ +import { DueDateInfo } from "@/data/dueDateInfo"; +import type { TaskTree } from "@/data/transformations/relationships"; +import { ShowMetadataVariant } from "@/query/query"; +import { useSettingsStore } from "@/settings"; +import Markdown from "@/ui/components/markdown"; +import { PluginContext, QueryContext } from "@/ui/context"; +import { TaskList } from "@/ui/query/task/TaskList"; +import { TaskMetadata } from "@/ui/query/task/TaskMetadata"; +import { showTaskContext } from "@/ui/query/task/contextMenu"; +import { motion } from "framer-motion"; +import { Notice } from "obsidian"; +import React, { type MouseEvent } from "react"; +import { Checkbox } from "react-aria-components"; + +type Props = { + tree: TaskTree; +}; + +export const Task: React.FC = ({ tree }) => { + const plugin = PluginContext.use(); + const query = QueryContext.use(); + const settings = useSettingsStore(); + + const onContextMenu = (ev: MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + showTaskContext({ task: tree, plugin }, { x: ev.pageX, y: ev.pageY }); + }; + + const onClickTask = async () => { + try { + await plugin.services.todoist.actions.closeTask(tree.id); + } catch (error: unknown) { + console.error("Failed to close task", error); + new Notice("Failed to close task", 2000); + } + }; + + const isDisabled = tree.content.startsWith("*"); + + const shouldRenderDescription = + settings.renderDescription && + query.show.has(ShowMetadataVariant.Description) && + tree.description !== ""; + + const transitionOpacity = settings.fadeToggle ? 0 : 1; + + return ( + <> + + +
    + +
    + + {shouldRenderDescription && } + +
    + + {tree.children.length > 0 && } + + ); +}; + +function getDueMetadataInfo(task: TaskTree): string | undefined { + const info = new DueDateInfo(task.due); + + if (info.isOverdue()) { + return "overdue"; + } + if (info.isToday()) { + return "today"; + } + if (info.isTomorrow()) { + return "tomorrow"; + } + + return undefined; +} + +function getTimeMetadataInfo(task: TaskTree): boolean { + return new DueDateInfo(task.due).hasTime(); +} + +const sanitizeContent = (content: string): string => { + // Escape leading '#' or '-' so they aren't rendered as headers/bullets. + if (content.startsWith("#") || content.startsWith("-")) { + return `\\${content}`; + } + + // A task starting with '*' signifies that it cannot be completed, so we should strip it from the front of the task. + if (content.startsWith("*")) { + return content.substring(1); + } + + return content; +}; + +type DescriptionRendererProps = { + content: string; +}; + +const DescriptionRenderer: React.FC = ({ content }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + + const isComplex = + content.includes("\n") || + content.startsWith("#") || + content.startsWith("*") || + content.startsWith("-") || + content.startsWith("1."); + + const toggleExpanded = () => { + if (!isComplex) { + return; + } + + setIsExpanded(!isExpanded); + }; + + const renderFullMarkdown = isExpanded || !isComplex; + + return ( +
    + {renderFullMarkdown ? ( + + ) : ( + {content.split("\n")[0]}... + )} +
    + ); +}; diff --git a/plugin/src/ui/query/task/TaskList.tsx b/plugin/src/ui/query/task/TaskList.tsx new file mode 100644 index 0000000..7f646c3 --- /dev/null +++ b/plugin/src/ui/query/task/TaskList.tsx @@ -0,0 +1,20 @@ +import type { TaskTree } from "@/data/transformations/relationships"; +import { Task } from "@/ui/query/task/Task"; +import { AnimatePresence } from "framer-motion"; +import React from "react"; + +type Props = { + trees: TaskTree[]; +}; + +export const TaskList: React.FC = ({ trees }) => { + return ( +
    + + {trees.map((tree) => ( + + ))} + +
    + ); +}; diff --git a/plugin/src/ui/query/task/TaskMetadata.tsx b/plugin/src/ui/query/task/TaskMetadata.tsx new file mode 100644 index 0000000..833d85e --- /dev/null +++ b/plugin/src/ui/query/task/TaskMetadata.tsx @@ -0,0 +1,128 @@ +import { DueDateInfo } from "@/data/dueDateInfo"; +import type { Task } from "@/data/task"; +import { type Query, ShowMetadataVariant } from "@/query/query"; +import type { Settings } from "@/settings"; +import { ObsidianIcon } from "@/ui/components/obsidian-icon"; +import type { CalendarSpec } from "moment"; +import React from "react"; + +type MetadataDefinition = { + name: string; + isShown: (query: Query, task: Task) => boolean; + content: (task: Task) => string; + icon: { + id: string; + shouldRender: (setting: Settings) => boolean; + orientation: "before" | "after"; + }; + side: "left" | "right"; +}; + +const metadata: MetadataDefinition[] = [ + { + name: "project", + isShown: (query, task) => query.show.has(ShowMetadataVariant.Project), + content: (task) => + task.section === undefined + ? task.project.name + : `${task.project.name} / ${task.section.name}`, + icon: { + id: "hash", + shouldRender: (settings) => settings.renderProjectIcon, + orientation: "after", + }, + side: "right", + }, + { + name: "due", + isShown: (query, task) => query.show.has(ShowMetadataVariant.Due) && task.due !== undefined, + content: (task) => dateLabel(task), + icon: { + id: "calendar", + shouldRender: (settings) => settings.renderDateIcon, + orientation: "before", + }, + side: "left", + }, + { + name: "labels", + isShown: (query, task) => query.show.has(ShowMetadataVariant.Labels) && task.labels.length > 0, + content: (task) => task.labels.join(", "), + icon: { + id: "tag", + shouldRender: (settings) => settings.renderLabelsIcon, + orientation: "before", + }, + side: "left", + }, +]; + +const dateLabel = (task: Task): string => { + const info = new DueDateInfo(task.due); + if (!info.hasDueDate()) { + return ""; + } + + const m = info.moment(); + + if (info.hasTime()) { + return m.calendar(); + } + + return m.calendar(dateOnlyCalendarSpec); +}; + +const dateOnlyCalendarSpec: CalendarSpec = { + sameDay: "[Today]", + nextDay: "[Tomorrow]", + nextWeek: "dddd", + lastDay: "[Yesterday]", + lastWeek: "[Last] dddd", + sameElse: "MMM Do", +}; + +type TaskMetadataProps = { + query: Query; + settings: Settings; + task: Task; +}; + +export const TaskMetadata: React.FC = (props) => { + const leftMetadata = getMetadataElems(props, "left"); + const rightMetadata = getMetadataElems(props, "right"); + + return ( +
    +
    {leftMetadata}
    +
    {rightMetadata}
    +
    + ); +}; + +const getMetadataElems = ( + props: TaskMetadataProps, + side: MetadataDefinition["side"], +): JSX.Element[] => { + const { query, task, settings } = props; + + return metadata + .filter((meta) => meta.side === side) + .filter((meta) => meta.isShown(query, task)) + .map((meta) => { + const content = {meta.content(task)}; + const icon = meta.icon.shouldRender(settings) ? ( + + ) : undefined; + + const children = [icon, content]; + if (meta.icon.orientation === "after") { + children.reverse(); + } + + return ( +
    + {children} +
    + ); + }); +}; diff --git a/plugin/src/contextMenu.ts b/plugin/src/ui/query/task/contextMenu.ts similarity index 67% rename from plugin/src/contextMenu.ts rename to plugin/src/ui/query/task/contextMenu.ts index 5971dff..4587850 100644 --- a/plugin/src/contextMenu.ts +++ b/plugin/src/ui/query/task/contextMenu.ts @@ -1,27 +1,27 @@ -import { Menu, Notice } from "obsidian"; +import type { Task } from "@/data/task"; +import type TodoistPlugin from "@/index"; +import { Menu } from "obsidian"; import type { Point } from "obsidian"; -import type { TaskId } from "./api/domain/task"; -import type { Task } from "./data/task"; -interface TaskContext { +type TaskContext = { task: Task; - closeTask: (id: TaskId) => Promise; -} + plugin: TodoistPlugin; +}; -export function showTaskContext(taskCtx: TaskContext, position: Point) { +export function showTaskContext(ctx: TaskContext, position: Point) { new Menu() .addItem((menuItem) => menuItem .setTitle("Complete task") .setIcon("check-small") - .onClick(async () => taskCtx.closeTask(taskCtx.task.id)), + .onClick(async () => await ctx.plugin.services.todoist.actions.closeTask(ctx.task.id)), ) .addItem((menuItem) => menuItem .setTitle("Open task in Todoist (app)") .setIcon("popup-open") .onClick(() => { - openExternal(`todoist://task?id=${taskCtx.task.id}`); + openExternal(`todoist://task?id=${ctx.task.id}`); }), ) .addItem((menuItem) => @@ -30,7 +30,7 @@ export function showTaskContext(taskCtx: TaskContext, position: Point) { .setIcon("popup-open") .onClick(() => openExternal( - `https://todoist.com/app/project/${taskCtx.task.project.id}/task/${taskCtx.task.id}`, + `https://todoist.com/app/project/${ctx.task.project.id}/task/${ctx.task.id}`, ), ), ) diff --git a/plugin/src/ui/settings/TokenChecker.tsx b/plugin/src/ui/settings/TokenChecker.tsx index 9ddd4fd..9e20d17 100644 --- a/plugin/src/ui/settings/TokenChecker.tsx +++ b/plugin/src/ui/settings/TokenChecker.tsx @@ -1,9 +1,9 @@ +import { PluginContext } from "@/ui/context"; import React, { useEffect, useState } from "react"; import { TodoistApiClient } from "../../api"; import { ObsidianFetcher } from "../../api/fetcher"; import { TokenValidation } from "../../token"; import { TokenValidationIcon } from "../components/token-validation-icon"; -import { usePluginContext } from "../context/plugin"; import { Setting } from "./SettingItem"; type Props = { @@ -11,7 +11,7 @@ type Props = { }; export const TokenChecker: React.FC = ({ tester }) => { - const plugin = usePluginContext(); + const plugin = PluginContext.use(); const { token: tokenAccessor, todoist, modals } = plugin.services; const [tokenState, setTokenState] = useState({ kind: "in-progress" }); diff --git a/plugin/src/ui/settings/index.tsx b/plugin/src/ui/settings/index.tsx index bee3414..455dc34 100644 --- a/plugin/src/ui/settings/index.tsx +++ b/plugin/src/ui/settings/index.tsx @@ -1,10 +1,10 @@ +import { PluginContext } from "@/ui/context"; import { App, PluginSettingTab } from "obsidian"; import React from "react"; import { type Root, createRoot } from "react-dom/client"; import type TodoistPlugin from "../.."; -import { type ISettings } from "../../settings"; +import { type Settings, useSettingsStore } from "../../settings"; import { TokenValidation } from "../../token"; -import { PluginContext } from "../context/plugin"; import { AutoRefreshIntervalControl } from "./AutoRefreshIntervalControl"; import { Setting } from "./SettingItem"; import { TokenChecker } from "./TokenChecker"; @@ -35,18 +35,20 @@ type Props = { }; type SettingsKeys = { - [K in keyof ISettings]: ISettings[K] extends V ? K : never; -}[keyof ISettings]; + [K in keyof Settings]: Settings[K] extends V ? K : never; +}[keyof Settings]; const SettingsRoot: React.FC = ({ plugin }) => { + const settings = useSettingsStore(); + const toggleProps = (key: SettingsKeys) => { const onClick = async (val: boolean) => { - await plugin.writeOptions((old) => { - old[key] = val; + await plugin.writeOptions({ + [key]: val, }); }; - const value = plugin.options[key]; + const value = settings[key]; return { value, @@ -55,8 +57,8 @@ const SettingsRoot: React.FC = ({ plugin }) => { }; const updateAutoRefreshInterval = async (val: number) => { - await plugin.writeOptions((old) => { - old.autoRefreshInterval = val; + await plugin.writeOptions({ + autoRefreshInterval: val, }); }; @@ -106,7 +108,7 @@ const SettingsRoot: React.FC = ({ plugin }) => { description="The interval, in seconds, that queries will be auto-refreshed by default" > diff --git a/plugin/styles.css b/plugin/styles.css index 51097f4..cd46cae 100644 --- a/plugin/styles.css +++ b/plugin/styles.css @@ -1,98 +1,24 @@ -.todoist-query-title { - display: inline; - font-size: 1.25em; -} - -.is-live-preview .todoist-refresh-button { - /* The refresh button sits next to the built-in 'Edit' button. So we need to place this excatly to the right of that one. But with --size-2-2 as buffer to it. */ - margin-right: calc(var(--icon-size) + 3 * var(--size-2-2)); -} - -.is-live-preview .todoist-add-button { - /* The add button is two buttons in, so we need to double the padding size */ - margin-right: calc(2 * var(--icon-size) + 6 * var(--size-2-2)); -} - -.markdown-reading-view .todoist-add-button, -.markdown-reading-view .todoist-refresh-button { - float: right; - margin-left: 8px; - color: var(--text-muted); - border-radius: var(--radius-s); - padding: var(--size-2-2) var(--size-2-3); -} - -.markdown-reading-view .todoist-add-button:hover, -.markdown-reading-view .todoist-refresh-button:hover { - background-color: var(--background-modifier-hover); -} - -.is-live-preview .todoist-refresh-button.todoist-refresh-fetching { - opacity: 1 !important; -} - -.todoist-refresh-fetching>div { - animation: spin 1s linear infinite reverse; -} - -@-webkit-keyframes spin { - 100% { - -webkit-transform: rotate(360deg); - } -} - -.markdown-reading-view .task-metadata, -.markdown-reading-view .todoist-task-description { - margin-left: 6px; -} - -.is-live-preview .task-metadata, -.is-live-preview .todoist-task-description { - margin-left: 34px; -} - -.task-metadata-item { - display: flex; - align-items: center; -} - -.task-metadata-item>*+* { - margin-left: 0.25rem; -} - -.task-metadata-item.task-overdue { - color: var(--text-error); -} - -.todoist-project { - white-space: normal; -} - -.todoist-error { - background-color: rgb(var(--background-modifier-error-rgb), 0.5); - border: 1px solid var(--background-modifier-error); - padding: 0 10px 20px 10px; - margin-top: 20px; -} - -.todoist-success { - border: 1px solid var(--background-modifier-border); - padding: 0 10px 20px 10px; - margin-top: 20px; -} - -.todoist-task-content { - display: inline; - text-indent: 0px; -} - -ul.todoist-task-list { - white-space: normal; - padding-inline-start: calc(var(--list-indent) - 11px); -} - -.markdown-reading-view .hide-in-reading-view { - display: none; +body { + --todoist-berry-red: #b8256f; + --todoist-red: #db4035; + --todoist-orange: #ff9933; + --todoist-yellow: #fad000; + --todoist-olive-green: #afb83b; + --todoist-lime-green: #7ecc49; + --todoist-green: #299438; + --todoist-mint-green: #6accbc; + --todoist-teal: #158fad; + --todoist-sky-blue: #14aaf5; + --todoist-light-blue: #96c3eb; + --todoist-blue: #4073ff; + --todoist-grape: #884dff; + --todoist-violet: #af38eb; + --todoist-lavender: #eb96eb; + --todoist-magenta: #e05194; + --todoist-salmon: #ff8d85; + --todoist-charcoal: #808080; + --todoist-grey: #b8b8b8; + --todoist-taupe: #ccac93; } .theme-dark { @@ -107,6 +33,12 @@ ul.todoist-task-list { --todoist-p3-border: #5297ff; --todoist-p3-border-hover: #5297ff80; --todoist-p3-background: rgba(82, 151, 255, .1); + + --todoist-p4-border: var(--color-base-50); + --todoist-p4-border-hover: var(--color-base-50); + --todoist-p4-background: unset; + + --todoist-task-separator-color: var(--color-base-30); } .theme-light { @@ -121,50 +53,10 @@ ul.todoist-task-list { --todoist-p3-border: #246fe0; --todoist-p3-border-hover: #246fe080; --todoist-p3-background: rgba(36, 111, 224, .1); -} - -.todoist-p1 input { - --checkbox-border-color: var(--todoist-p1-border); - --checkbox-border-color-hover: var(--todoist-p1-border-hover); - background-color: var(--todoist-p1-background); -} - -.todoist-p2 input { - --checkbox-border-color: var(--todoist-p2-border); - --checkbox-border-color-hover: var(--todoist-p2-border-hover); - background-color: var(--todoist-p2-background); -} - -.todoist-p3 input { - --checkbox-border-color: var(--todoist-p3-border); - --checkbox-border-color-hover: var(--todoist-p3-border-hover); - background-color: var(--todoist-p3-background); -} - -.todoist-task-description { - font-size: 80%; - color: var(--text-muted); -} - -.todoist-group-title { - margin: 1em 0; - font-weight: 600; -} - -.todoist-query-warnings { - padding: 16px; - background-color: rgba(var(--color-yellow-rgb), 0.2); - border-radius: 4px; -} - -.todoist-query-warnings-header { - display: flex; - align-items: center; - margin-bottom: 4px; - font-weight: 600; -} + --todoist-p4-border: var(--color-base-50); + --todoist-p4-border-hover: var(--color-base-50); + --todoist-p4-background: unset; -.todoist-query-warnings-icon { - margin-right: 0.5em; + --todoist-task-separator-color: var(--color-base-25); } \ No newline at end of file diff --git a/plugin/svelte.config.mjs b/plugin/svelte.config.mjs deleted file mode 100644 index 8abe436..0000000 --- a/plugin/svelte.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -export default { - preprocess: vitePreprocess(), -} diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json index 02418a7..6de3820 100644 --- a/plugin/tsconfig.json +++ b/plugin/tsconfig.json @@ -1,14 +1,18 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", - "include": ["src/**/*", "./vitest-setup.ts"], "exclude": ["node_modules/*"], "compilerOptions": { "jsx": "react", "esModuleInterop": true, - "types": ["node", "svelte"], + "types": ["node"], "paths": { "@/*": ["./src/*"] - } + }, + "module": "esnext", + "moduleResolution": "bundler", + "target": "es2017", + "verbatimModuleSyntax": true, + "strict": true, + "skipLibCheck": true } } diff --git a/plugin/vite.config.mts b/plugin/vite.config.mts index ad2abf4..c0176ca 100644 --- a/plugin/vite.config.mts +++ b/plugin/vite.config.mts @@ -1,6 +1,5 @@ import path, { resolve } from "path"; import replace from "@rollup/plugin-replace"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; import { loadEnv } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import tsConfigPaths from "vite-tsconfig-paths"; @@ -23,9 +22,6 @@ function getOutDir(): string | undefined { export default defineConfig({ plugins: [ tsConfigPaths(), - svelte({ - emitCss: false, - }), viteStaticCopy({ targets: [ {