Major new feature: set goals

This commit is contained in:
Tim 2025-11-22 23:42:44 +00:00
parent be3b46c578
commit 95dbfbe6f1
20 changed files with 2954 additions and 479 deletions

2
.gitignore vendored
View file

@ -17,7 +17,7 @@ Thumbs.db
.vscode/
.idea/
*.swp
calendar-period-week-notes.code-workspace
calendar-period-week-notes.code-workspace
#Local Scripts
deploy-to-obsidian.sh

View file

@ -1,7 +1,7 @@
{
"id": "calendar-period-week-notes",
"name": "Calendar Period Week Notes",
"version": "1.4.2",
"version": "1.5.0",
"minAppVersion": "0.15.0",
"description": "A calendar grid plugin with a scratchpad area, notes list, pinned notes, tasks and asset views.",
"author": "Tim Williams",

353
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "calendar-period-week-notes",
"version": "1.4.2",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "calendar-period-week-notes",
"version": "1.4.2",
"version": "1.5.0",
"license": "MIT",
"dependencies": {
"ical.js": "^2.2.1",
@ -21,15 +21,11 @@
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.9",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz",
"integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -55,8 +51,6 @@
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -76,8 +70,6 @@
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -101,8 +93,6 @@
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -122,38 +112,8 @@
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
@ -164,297 +124,23 @@
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"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,
"license": "MIT",
"engines": {
@ -463,15 +149,11 @@
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
@ -488,10 +170,7 @@
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
@ -503,8 +182,6 @@
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
@ -513,8 +190,6 @@
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -526,14 +201,10 @@
},
"node_modules/ical.js": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.2.1.tgz",
"integrity": "sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==",
"license": "MPL-2.0"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -548,15 +219,11 @@
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true,
"license": "MIT"
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -565,8 +232,6 @@
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -575,8 +240,6 @@
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
@ -584,18 +247,13 @@
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -605,8 +263,6 @@
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -626,11 +282,8 @@
},
"node_modules/rollup": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@ -669,8 +322,6 @@
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {

View file

@ -1,6 +1,6 @@
{
"name": "calendar-period-week-notes",
"version": "1.4.2",
"version": "1.5.0",
"description": "A calendar based Obsidian plugin.",
"main": "main.js",
"type": "module",

View file

@ -3,13 +3,17 @@ export const VIEW_TYPE_PERIOD = "calendar-period-week";
export const DASHBOARDWIDGETS = {
tasks: {
weeklyGoalPoints: { name: 'Weekly points score (Full)' },
goalStatusList: { name: 'Daily goals (Full)' },
goalStatusListCondensed: { name: 'Daily goals (Half-width)', default: false },
weeklyGoalPointsCondensed: { name: 'Weekly momentum (Half-width)', default: false },
today: { name: 'Today' },
tomorrow: { name: 'Tomorrow' },
next7days: { name: 'Next 7 days' },
futureNoDue: { name: 'Future / No due' },
upcomingoverdue: { name: 'Upcoming & overdue tasks' },
taskstatusoverview: { name: 'Task status overview' },
taskcompletionheatmap: { name: 'Task completion activity' }
taskcompletionheatmap: { name: 'Task completion activity' },
},
creation: {
allfilesheatmap: { name: 'All files' },
@ -253,7 +257,8 @@ export const DEFAULT_SETTINGS = {
regularnotesheatmap: true,
assetsheatmap: true,
},
tasksDashboardOrder: ['today', 'tomorrow', 'next7days', 'futureNoDue', 'upcomingoverdue', 'taskstatusoverview', 'taskcompletionheatmap'],
tasksDashboardOrder: ['Today\'s goals', 'Weekly points score', 'today', 'tomorrow', 'next7days', 'futureNoDue', 'upcomingoverdue', 'taskstatusoverview', 'taskcompletionheatmap'],
tasksDashboardWidgets: {
today: true,
tomorrow: true,
@ -269,9 +274,38 @@ export const DEFAULT_SETTINGS = {
isHeatmapGuideCollapsed: true,
heatmapLinkConfig: {},
heatmapTotalClickOpenAction: 'new-tab',
customHeatmaps: [],
goals: [
{
id: 'core-daily-note',
name: 'Daily note created',
type: 'note-created',
target: 1,
points: 60,
core: true,
enabled: true
},
{
id: 'core-words-written',
name: 'Words written',
type: 'word-count',
target: 200,
points: 24,
core: true,
enabled: true
},
{
id: 'core-tasks-completed',
name: 'Tasks completed',
type: 'task-count',
target: 1,
points: 30,
core: true,
enabled: true
}
],
goalScoreHistory: {},
vacationHistory: {},
// In your DEFAULT_SETTINGS
tasksStatsWidgets: [
{
"widgetKey": "custom-completed-line-30d",

View file

@ -13,7 +13,6 @@ export const BUNDLED_TEMPLATES = [
line,
pie,
radar,
// Add other imported templates to this array
];

393
src/logic/GoalTracker.js Normal file
View file

@ -0,0 +1,393 @@
import { moment, TFile } from 'obsidian';
export class GoalTracker {
constructor(app, settings) {
this.app = app;
this.settings = settings;
if (!this.settings.goalScoreHistory) {
this.settings.goalScoreHistory = {};
}
if (!this.settings.vacationHistory) {
this.settings.vacationHistory = {};
}
}
/**
* Checks a single goal and returns its status, including current progress.
* @param {object} goal - The goal configuration object.
* @param {moment.Moment} date - The date to check against.
* @param {Array} allTasks - The complete list of tasks.
* @returns {Promise<{isMet: boolean, current: number}>}
*/
async checkGoal(goal, date, allTasks) {
let result = { isMet: false, current: 0 };
switch (goal.type) {
case 'note-created':
result = await this.checkNoteCreated(date, goal.folder);
break;
case 'word-count':
result = await this.checkWordCount(date, goal.folder, goal.target);
break;
case 'task-count':
result = this.checkTaskCount(date, allTasks, goal.query, goal.target);
break;
}
return result;
}
/**
* Calculates the total score for a day with bonuses, active-day rules,
* vacation mode, and persistent caching.
* @param {moment.Moment} date - The date to score.
* @param {Array} allTasks - The complete list of tasks.
* @returns {Promise<{date: string, totalPoints: number, results: Array<object>, allMet: boolean, level: number, levelTitle: string, levelIcon: string}>}
*/
async calculateDailyScore(date, allTasks = []) {
if (this.settings.vacationModeGlobal) {
// Vacation applies to all goals, skip scoring
return {
date: date.format('YYYY-MM-DD'),
totalPoints: 0,
results: [],
allMet: false,
level: 0,
levelTitle: '',
levelIcon: ''
};
}
const isoKey = date.format('YYYY-MM-DD');
const todayStart = moment().startOf('day');
const dateStart = date.clone().startOf('day');
const canUseCache = dateStart.isBefore(todayStart, 'day');
// 0. Return cached snapshot if available
if (canUseCache &&
this.settings.goalScoreHistory &&
this.settings.goalScoreHistory[isoKey]) {
return this.settings.goalScoreHistory[isoKey];
}
// Ensure containers exist
if (!this.settings.goalScoreHistory) this.settings.goalScoreHistory = {};
if (!this.settings.vacationHistory) this.settings.vacationHistory = {};
const vacationForDay = this.settings.vacationHistory[isoKey] || {};
let totalPoints = 0;
const results = [];
const goals = this.settings.goals || [];
const weekdayIndex = date.isoWeekday() - 1; // 0=Mon .. 6=Sun
let activeGoalsCount = 0;
let metGoalsCount = 0;
const activeGoals = (this.settings.goals || []).filter(g => !!g.enabled);
for (const goal of activeGoals) {
const activeDays = Array.isArray(goal.activeDays) && goal.activeDays.length > 0
? goal.activeDays
: [0, 1, 2, 3, 4, 5, 6];
const isActiveToday = activeDays.includes(weekdayIndex);
// Historical vacation: if this goal was ever on vacation for this date,
// keep that regardless of the current toggle state.
const isVacationToday =
!!vacationForDay[goal.id] || !!goal.vacationMode === true;
// If the goal is in vacationMode *now* and the day is being scored,
// persist that into vacationHistory so future recalcs remember it.
if (isActiveToday && goal.vacationMode) {
if (!this.settings.vacationHistory[isoKey]) {
this.settings.vacationHistory[isoKey] = {};
}
this.settings.vacationHistory[isoKey][goal.id] = true;
}
let result = { isMet: false, current: 0 };
if (isActiveToday && !isVacationToday) {
result = await this.checkGoal(goal, date, allTasks);
activeGoalsCount++;
} else if (isActiveToday && isVacationToday) {
// still "active" for display purposes but we won't penalise
result = await this.checkGoal(goal, date, allTasks);
activeGoalsCount++;
}
let goalPoints = 0;
if (isActiveToday && result.isMet && !isVacationToday) {
metGoalsCount++;
const baseValue = parseInt(goal.points || 0);
goalPoints = baseValue;
if (goal.type === 'task-count') {
const extra = Math.max(0, result.current - goal.target);
if (extra > 0) {
goalPoints += extra * 2;
}
}
} else if (isActiveToday && !result.isMet && !isVacationToday) {
// PENALTY for missing a goal, unless vacation is recorded
goalPoints = -10;
} else {
// Not active today or vacation → neutral
goalPoints = 0;
}
totalPoints += goalPoints;
results.push({
id: goal.id,
name: goal.name,
isMet: isActiveToday ? result.isMet : false,
isActiveToday,
isVacationToday,
points: goalPoints,
target: goal.target,
current: result.current,
type: goal.type
});
}
// 2. "All Goals Met" Daily Bonus (only active goals count)
const allGoalsMet = activeGoalsCount > 0 && metGoalsCount === activeGoalsCount;
if (allGoalsMet) {
totalPoints += 100;
}
// 3. Small Goal Count Multiplier (12 active goals)
if (activeGoalsCount > 0 && activeGoalsCount < 3) {
totalPoints = Math.floor(totalPoints * 1.5);
}
// 4. Streak Bonus (Check Yesterday Only, using active goals)
if (allGoalsMet) {
const yesterday = date.clone().subtract(1, 'days');
const yesterdayResults = await this.calculateDailyScoreSimple(yesterday, allTasks);
if (yesterdayResults.allMet) {
totalPoints += 50;
}
}
// 5. Get Achievement Level for this day's score
const achievement = this.getAchievementLevel(totalPoints);
const resultObj = {
date: isoKey,
totalPoints,
results,
allMet: allGoalsMet,
level: achievement.level,
levelTitle: achievement.title,
levelIcon: achievement.icon
};
// 6. Persist snapshot in cache for past days
if (canUseCache) {
if (!this.settings.goalScoreHistory) {
this.settings.goalScoreHistory = {};
}
this.settings.goalScoreHistory[isoKey] = {
...resultObj,
cachedAt: Date.now()
};
}
return resultObj;
}
// Simplified calculation for "Yesterday" to avoid infinite loops/deep recursion
async calculateDailyScoreSimple(date, allTasks) {
const activeGoals = (this.settings.goals || []).filter(g => !!g.enabled);
if (activeGoals.length === 0) return { allMet: false };
let met = 0;
for (const goal of activeGoals) {
const res = await this.checkGoal(goal, date, allTasks);
if (res.isMet) met++;
}
return { allMet: met === activeGoals.length };
}
// --- HELPER TO GET DAILY NOTE PATH ---
getDailyNotePath(date, overrideFolder) {
let folder = overrideFolder || '';
let format = 'YYYY-MM-DD';
// 1. Try Periodic Notes (Community Plugin)
// Attempt to detect Periodic Notes plugin (community plugin)
const periodicNotes = this.app.plugins.plugins['periodic-notes'];
if (periodicNotes && periodicNotes.settings?.daily?.enabled) {
const daily = periodicNotes.settings.daily;
if (!folder) folder = daily.folder || '';
format = daily.format || 'YYYY-MM-DD';
}
else {
// 2. Try Core Daily Notes Plugin
const dailyNotes = this.app.internalPlugins.getPluginById('daily-notes');
if (dailyNotes && dailyNotes.instance && dailyNotes.instance.options) {
const options = dailyNotes.instance.options;
if (!folder) folder = options.folder || '';
format = options.format || 'YYYY-MM-DD';
}
}
// 3. Fallback
if (!format) format = 'YYYY-MM-DD';
// 4. Construct Path
const filename = date.format(format);
const normalizedFolder = folder ? folder.trim().replace(/^\/|\/$/g, '') : '';
const fullPath = normalizedFolder ? `${normalizedFolder}/${filename}.md` : `${filename}.md`;
return fullPath;
}
/**
* Checks if a daily note exists.
*/
async checkNoteCreated(date, folderPath) {
const path = this.getDailyNotePath(date, folderPath);
const file = this.app.vault.getAbstractFileByPath(path);
const exists = file instanceof TFile;
return { isMet: exists, current: exists ? 1 : 0 };
}
/**
* Checks word count in a daily note.
*/
async checkWordCount(date, folderPath, target) {
const path = this.getDailyNotePath(date, folderPath);
const file = this.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) {
return { isMet: false, current: 0 };
}
const content = await this.app.vault.read(file);
const contentBody = content.replace(/^---[\s\S]+?---/, ''); // Strip frontmatter
const words = contentBody.trim().split(/\s+/).filter(w => w.length > 0).length;
return { isMet: words >= target, current: words };
}
/**
* Checks completed task count.
*/
checkTaskCount(date, allTasks, tagQuery, target) {
if (!allTasks) return { isMet: false, current: 0 };
const targetDateStart = date.clone().startOf('day');
const targetDateEnd = date.clone().endOf('day');
const completed = allTasks.filter(t => {
const isDone = t.status === 'x' || t.status?.type === 'DONE';
if (!isDone) return false;
const completionDate = t.done?.moment || (t.completionDate ? moment(t.completionDate) : null);
if (!completionDate) return false;
const isSameDay = completionDate.isBetween(targetDateStart, targetDateEnd, null, '[]');
const matchesTag = tagQuery
? (t.tags && t.tags.some(tag => tag.includes(tagQuery.replace('#', ''))))
: true;
return isSameDay && matchesTag;
});
return { isMet: completed.length >= target, current: completed.length };
}
getAchievementLevel(points) {
const LEVEL_BANDS = [
{ frac: 1.00, title: "Grandmaster of Flow", icon: "infinity" },
{ frac: 0.90, title: "Legendary Leader", icon: "crown" },
{ frac: 0.80, title: "Relentless Result", icon: "trophy" },
{ frac: 0.70, title: "Task Titan", icon: "medal" },
{ frac: 0.60, title: "Output Overlord", icon: "award" },
{ frac: 0.50, title: "High Performer", icon: "star" },
{ frac: 0.40, title: "Productivity Pro", icon: "zap" },
{ frac: 0.30, title: "Goal Getter", icon: "target" },
{ frac: 0.22, title: "Focused Ninja", icon: "crosshair" },
{ frac: 0.16, title: "Efficiency Enthusiast", icon: "gauge" },
{ frac: 0.10, title: "Daily Driver", icon: "calendar-check" },
{ frac: 0.06, title: "Momentum Maker", icon: "trending-up" },
{ frac: 0.03, title: "Task Tinkerer", icon: "check-circle-2" },
{ frac: 0.01, title: "Routine Rookie", icon: "clipboard-list" },
{ frac: 0.00, title: "Starter Spark", icon: "sprout" }
];
//const target = this.getLifetimeTargetPoints();
const target = this.getLifetimeTargetPoints() || 300000;
if (!target) {
// Fallback: no goals yet
return { min: 0, title: "Starter Spark", level: 1, icon: "sprout" };
}
const levels = LEVEL_BANDS
.map((band, idx) => ({
min: Math.round(band.frac * target),
title: band.title,
level: LEVEL_BANDS.length - idx,
icon: band.icon
}))
// IMPORTANT: sort from highest min to lowest
.sort((a, b) => b.min - a.min);
// Find the highest band whose min you meet
const match = levels.find(l => points >= l.min);
return match || levels[levels.length - 1];
}
static async getLifetimePoints(app, settings, allTasks) {
const history = settings.goalScoreHistory || {};
let total = 0;
for (const dateKey in history) {
total += history[dateKey].totalPoints || 0;
}
return total;
}
getLifetimeTargetPoints() {
const idealDaily = this.getIdealDailyMax();
if (!idealDaily) return 0;
// Assume a typical user hits ~80% of ideal on average
const typicalDaily = idealDaily * 0.8;
const daysForMaxLevel = 3 * 365; // 3 years
return Math.round(typicalDaily * daysForMaxLevel);
}
getIdealDailyMax() {
const goals = this.settings.goals || [];
if (goals.length === 0) return 0;
// Sum configured points for all goals
let base = goals.reduce((acc, g) => acc + (parseInt(g.points || 0) || 0), 0);
// Assume "all goals met" bonus
base += 100;
// If user has only 12 goals, they still get the small multiplier
if (goals.length < 3) {
base = Math.floor(base * 1.5);
}
// Assume they're in a streak on a good day
base += 50;
return base; // this is "max-ish" points for a really good day
}
}

View file

@ -1,7 +1,4 @@
/* --cpwn-- Obsidian Plugin: Periodic Notes Calendar View --cpwn--
Developer: Tim Williams
This plugin uses library: https://app.unpkg.com/ical.js@2.2.1/files/dist/ical.es5.min.cjs (c) 2024 Chris Lawley, MIT License
*/
/* -- Obsidian Plugin: Calendar Periodic Week Notes -- */
import { Plugin, debounce } from 'obsidian';
import { PeriodMonthView, VIEW_TYPE_PERIOD } from './views/PeriodMonthView.js';
@ -21,6 +18,50 @@ export default class PeriodMonthPlugin extends Plugin {
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const CORE_GOALS = [
{
id: 'core-daily-note',
name: 'Daily note created',
type: 'note-created',
target: 1,
points: 60,
core: true,
enabled: true
},
{
id: 'core-words-written',
name: 'Words written',
type: 'word-count',
target: 200,
points: 24,
core: true,
enabled: true
},
{
id: 'core-tasks-completed',
name: 'Tasks completed',
type: 'task-count',
target: 2,
points: 60,
core: true,
enabled: true
}
];
// Make sure the goals property exists
if (!this.settings.goals || this.settings.goals.length === 0) {
this.settings.goals = [...CORE_GOALS];
}
// Always check and inject missing core goals before showing UI
CORE_GOALS.forEach(coreGoal => {
const found = this.settings.goals.some(g => g.id === coreGoal.id && g.core);
if (!found) {
this.settings.goals.unshift({ ...coreGoal });
}
});
this.updateStyles();
this.registerView(VIEW_TYPE_PERIOD, leaf => new PeriodMonthView(leaf, this));
this.addRibbonIcon("calendar-check", "Open Calendar Period Week Notes", () => this.activateView());
@ -57,16 +98,6 @@ export default class PeriodMonthPlugin extends Plugin {
await this.saveData(this.settings);
}
/**
* Injects the plugin's CSS into the document head.
*/
addStyle() {
const styleEl = document.createElement('style');
styleEl.id = 'cpwn-period-month-plugin-styles';
styleEl.textContent = PLUGIN_STYLES;
document.head.appendChild(styleEl);
}
/**
* Updates CSS custom properties (variables) based on the current plugin settings.
*/
@ -192,7 +223,7 @@ export default class PeriodMonthPlugin extends Plugin {
}
async loadTemplates() {
return Promise.resolve(BUNDLED_TEMPLATES);
}

252
src/modals/GoalEditModal.js Normal file
View file

@ -0,0 +1,252 @@
import { Modal, Setting, Notice, AbstractInputSuggest, TFolder } from 'obsidian';
// --- 1. Define FolderSuggest Helper Class ---
class FolderSuggest extends AbstractInputSuggest {
constructor(app, textInputEl) {
super(app, textInputEl);
this.app = app;
}
getSuggestions(inputStr) {
const abstractFiles = this.app.vault.getAllLoadedFiles();
const folders = [];
const lowerCaseInputStr = inputStr.toLowerCase();
for (const file of abstractFiles) {
if (file instanceof TFolder) {
if (file.path.toLowerCase().includes(lowerCaseInputStr)) {
folders.push(file);
}
}
}
return folders;
}
renderSuggestion(file, el) {
el.setText(file.path);
}
selectSuggestion(file) {
this.inputEl.value = file.path;
this.inputEl.trigger("input");
this.close();
}
}
// --- 2. Your GoalEditModal Class ---
export class GoalEditModal extends Modal {
constructor(app, existingGoal, onSave) {
super(app);
this.goal = existingGoal || {
id: crypto.randomUUID(),
name: '',
type: 'task-count',
target: 1,
points: 10,
query: '',
folder: '',
activeDays: [0, 1, 2, 3, 4, 5, 6],
vacationMode: false
};
this.tempGoal = { ...this.goal };
// Ensure new fields exist on older goals
if (!Array.isArray(this.tempGoal.activeDays) || this.tempGoal.activeDays.length === 0) {
this.tempGoal.activeDays = [0, 1, 2, 3, 4, 5, 6];
}
if (typeof this.tempGoal.vacationMode !== 'boolean') {
this.tempGoal.vacationMode = false;
}
this.onSave = onSave;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: this.goal.name ? 'Edit goal' : 'Create new goal' });
new Setting(contentEl)
.setDesc('If you change a folder location, #tags or goal values to achieve it\'s recommended to perform a \'Reset cache\' to recalculate your progress.')
// 1. Goal Name
new Setting(contentEl)
.setName('Goal name')
.setDesc('e.g., "Morning writing" or "Daily tasks"')
.addText(text => text
.setValue(this.tempGoal.name)
.onChange((value) => {
this.tempGoal.name = value;
}));
// 2. Goal Type Dropdown
new Setting(contentEl)
.setName('Goal type')
.addDropdown(drop => drop
.addOption('task-count', 'Daily tasks')
.addOption('note-created', 'Daily note created')
.addOption('word-count', 'Daily word count')
.setValue(this.tempGoal.type)
.onChange(value => {
this.tempGoal.type = value;
// Set sensible defaults
if (value === 'note-created') {
this.tempGoal.target = 1;
} else if (value === 'word-count' && this.tempGoal.target < 100) {
this.tempGoal.target = 500;
} else if (value === 'task-count' && this.tempGoal.target > 50) {
this.tempGoal.target = 3;
}
this.updatePointsPreview();
this.display();
}));
// 3. Conditional Input: Tag Query
if (this.tempGoal.type === 'task-count') {
new Setting(contentEl)
.setName('Filter by tag (optional)')
.setDesc('Only count tasks with this tag (e.g., #gym). Leave blank for all tasks.')
.addText(text => text
.setPlaceholder('#tag')
.setValue(this.tempGoal.query)
.onChange(value => this.tempGoal.query = value));
}
// 4. Conditional Input: Folder Picker WITH DROPDOWN
if (this.tempGoal.type === 'word-count' || this.tempGoal.type === 'note-created') {
new Setting(contentEl)
.setName("Folder location (optional)")
.setDesc("Leave blank to use your default Daily Notes folder, or specify a folder.")
.addText(text => {
text.setPlaceholder("Example: Journals")
.setValue(this.tempGoal.folder || "")
.onChange(value => {
this.tempGoal.folder = value;
});
// --- ATTACH FOLDER SUGGESTER HERE ---
new FolderSuggest(this.app, text.inputEl);
});
}
// 5. Conditional Input: Target Value
if (this.tempGoal.type !== 'note-created') {
new Setting(contentEl)
.setName("Target value")
.setDesc(this.tempGoal.type === 'word-count' ? "How many words?" : "How many tasks?")
.addText(text => text
.setValue(String(this.tempGoal.target))
.onChange((value) => {
this.tempGoal.target = Number(value) || 0;
this.updatePointsPreview();
}));
}
// 5.5 Active days picker
const daysSetting = new Setting(contentEl)
.setName("Active days")
.setDesc("Points and penalties only apply on these days.");
const daysContainer = daysSetting.controlEl.createDiv({
cls: 'cpwn-goal-days-picker'
});
const dayLabels = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
dayLabels.forEach((label, idx) => {
const dayEl = daysContainer.createSpan({
text: label,
cls: 'cpwn-goal-day'
});
if (this.tempGoal.activeDays.includes(idx)) {
dayEl.addClass('is-active');
}
dayEl.addEventListener('click', () => {
const active = this.tempGoal.activeDays;
const pos = active.indexOf(idx);
if (pos === -1) {
active.push(idx);
dayEl.addClass('is-active');
} else {
if (active.length > 1) {
active.splice(pos, 1);
dayEl.removeClass('is-active');
}
}
});
});
// 5.6 Vacation mode toggle
new Setting(contentEl)
.setName("Vacation mode")
.setDesc("When enabled, missed days for this goal give no penalties.")
.addToggle(toggle => {
toggle
.setValue(!!this.tempGoal.vacationMode)
.onChange((value) => {
this.tempGoal.vacationMode = value;
});
});
// 6. Points Display
const pointsSetting = new Setting(contentEl)
.setName("Points reward details")
.setDesc("Points are calculated automatically based on your target.");
this.pointsInfoEl = pointsSetting.controlEl.createDiv({ cls: 'cpwn-points-info' });
this.pointsInfoEl.style.textAlign = 'right';
this.pointsInfoEl.style.fontSize = '0.9em';
this.pointsInfoEl.style.color = 'var(--text-accent)';
this.updatePointsPreview();
// 7. Save Button
new Setting(contentEl)
.addButton(btn => btn
.setButtonText("Save goal")
.setCta()
.onClick(() => {
if (!this.tempGoal.name) {
new Notice('Please enter a goal name.');
return;
}
this.onSave(this.tempGoal);
this.close();
}));
}
updatePointsPreview() {
let points = 0;
let explanation = "";
if (this.tempGoal.type === 'note-created') {
points = 60;
explanation = "Reward: 60 pts\n(Penalty: -10 pts if missed)";
} else if (this.tempGoal.type === 'task-count') {
const count = this.tempGoal.target || 0;
points = count * 30;
explanation = `${count} tasks × 30 pts = ${points} pts\n(+2 pts per extra task)\n(Penalty: -10 pts if target not met)`;
} else if (this.tempGoal.type === 'word-count') {
const count = this.tempGoal.target || 0;
points = Math.floor((count / 250) * 30);
explanation = `${count} words ≈ ${points} pts\n(30 pts per 250 words)\n(Penalty: -10 pts if target not met)`;
}
if (this.pointsInfoEl) {
this.pointsInfoEl.innerText = explanation;
this.pointsInfoEl.style.whiteSpace = 'pre-line';
}
this.tempGoal.points = points;
}
display() {
this.onOpen();
}
}

View file

@ -26,7 +26,7 @@ export class SectionHeaderModal extends Modal {
: defaultConfig;
}
async onOpen() {
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: 'Create section header' });
@ -44,7 +44,7 @@ export class SectionHeaderModal extends Modal {
new Setting(contentEl)
.setName('Visible')
.setDesc('Toggle whether this section header is visible on the dashboard. Having the section header enabled but hidden allows you to use it for grouping widgets without affecting if a section header that\s above is being toggled to show and hide over time, i.e. this section of grouped widgets will always be visible.')
.setDesc('Toggle whether this section header is visible on the dashboard. Having the section header enabled but hidden allows you to use it for grouping widgets without affecting if a section header that\'s above is being toggled to show and hide over time, i.e. this section of grouped widgets will always be visible.')
.addToggle(toggle => toggle
.setValue(this.config.isVisible ?? true) // Default to true if not set
.onChange(value => {

View file

@ -95,7 +95,7 @@ export class TemplatePickerModal extends Modal {
ctx.clearRect(0, 0, width, height); // Clear canvas before drawing
ctx.font = "48px sans-serif"; // Font size and family
ctx.fillStyle = template.previewData.datasets[0].color || "#4caf50";; // Text color (KPI color if you wish)
ctx.fillStyle = template.previewData.datasets[0].color || "#4caf50";
ctx.textAlign = "center"; // Center horizontally
ctx.textBaseline = "middle"; // Center vertically

View file

@ -71,7 +71,7 @@ export class WidgetBuilderModal extends Modal {
}
// Clear and add the correct options
legendPositionDropdown.selectEl.innerHTML = '';
llegendPositionDropdown.selectEl.empty();
legendPositionDropdown.addOptions(newOptions);
// Reset position if the old one is invalid

View file

@ -14,6 +14,8 @@ import { PeriodMonthView } from '../views/PeriodMonthView.js';
import { WidgetBuilderModal } from '../modals/WidgetBuilderModal.js';
import { SectionHeaderModal } from '../modals/SectionHeaderModal.js';
import { TemplatePickerModal } from '../modals/TemplatePickerModal.js';
import { GoalEditModal } from '../modals/GoalEditModal.js';
import { GoalTracker } from '../logic/GoalTracker';
/**
* The settings tab class for the plugin, responsible for building the settings UI.
@ -1103,6 +1105,7 @@ export class PeriodSettingsTab extends PluginSettingTab {
assets: "Assets tab",
tasks: "Tasks tab",
dashboard: 'Dashboard tab',
goals: 'Goal configuration',
themes: "Themes",
importExport: "Import / Export",
startHere: "Help",
@ -1252,6 +1255,9 @@ export class PeriodSettingsTab extends PluginSettingTab {
case 'dashboard':
this.renderDashboardSettings();
break;
case 'goals':
this.renderGoalsSettings();
break;
case 'themes':
this.renderThemesSettings();
break;
@ -2404,6 +2410,296 @@ export class PeriodSettingsTab extends PluginSettingTab {
tasksBtn.click();
}
renderGoalsSettings() {
const containerEl = this.contentEl;
containerEl.empty();
containerEl.createEl("h1", { text: "Goal configuration" });
containerEl.createEl("p", { text: "Define goals to track your progress and earn points. Drag to reorder." });
const goalsList = containerEl.createDiv({ cls: 'cpwn-goals-list' });
this.enableGoalSorting(goalsList);
const allGoals = this.plugin.settings.goals || [];
const coreGoals = allGoals.filter(g => g.core);
const customGoals = allGoals.filter(g => !g.core);
// --- Render core goals first ---
coreGoals.forEach((goal, index) =>
this.renderGoalItem(goalsList, goal, allGoals.indexOf(goal), true)
);
// --- Render Custom Goals ---
customGoals.forEach((goal, index) => {
this.renderGoalItem(goalsList, goal, allGoals.indexOf(goal), false);
});
// --- Add New Goal Button ---
new Setting(containerEl)
.addButton(btn => {
btn.setButtonText("Add new goal")
.setCta()
.onClick(() => {
new GoalEditModal(this.app, null, async (newGoal) => {
if (!this.plugin.settings.goals) this.plugin.settings.goals = [];
this.plugin.settings.goals.push(newGoal);
await this.saveAndUpdate();
this.renderGoalsSettings();
}).open();
});
});
// --- Global Vacation Toggle ---
new Setting(containerEl)
.setName("Global Vacation Mode")
.setDesc("When enabled, all goals are paused — no points or penalties are tracked. Note: you can also pause goals individually. To do this enter the goal settings to toggle on/off.")
.addToggle(toggle => {
toggle.setValue(!!this.plugin.settings.vacationModeGlobal)
.onChange(async (value) => {
this.plugin.settings.vacationModeGlobal = value;
await this.saveAndUpdate();
});
});
containerEl.createEl('h3', { text: 'Level ladder' });
const tracker = new GoalTracker(this.app, this.plugin.settings);
const lifetimeTargetRaw =
tracker.getLifetimeTargetPoints && tracker.getLifetimeTargetPoints();
const effectiveTarget =
lifetimeTargetRaw && lifetimeTargetRaw > 0 ? lifetimeTargetRaw : 300000;
// Same bands you use in the widget / GoalTracker
const LEVEL_BANDS = [
{ frac: 1.0, title: 'Grandmaster of Flow', icon: 'infinity' },
{ frac: 0.9, title: 'Legendary Leader', icon: 'crown' },
{ frac: 0.8, title: 'Relentless Result', icon: 'trophy' },
{ frac: 0.7, title: 'Task Titan', icon: 'medal' },
{ frac: 0.6, title: 'Output Overlord', icon: 'award' },
{ frac: 0.5, title: 'High Performer', icon: 'star' },
{ frac: 0.4, title: 'Productivity Pro', icon: 'zap' },
{ frac: 0.3, title: 'Goal Getter', icon: 'target' },
{ frac: 0.22, title: 'Focused Ninja', icon: 'crosshair' },
{ frac: 0.16, title: 'Efficiency Enthusiast', icon: 'gauge' },
{ frac: 0.10, title: 'Daily Driver', icon: 'calendar-check' },
{ frac: 0.06, title: 'Momentum Maker', icon: 'trending-up' },
{ frac: 0.03, title: 'Task Tinkerer', icon: 'check-circle-2' },
{ frac: 0.01, title: 'Routine Rookie', icon: 'clipboard-list' },
{ frac: 0.0, title: 'Starter Spark', icon: 'sprout' }
];
const levels = LEVEL_BANDS
.map((band, idx) => ({
min: Math.round(band.frac * effectiveTarget),
title: band.title,
icon: band.icon,
level: LEVEL_BANDS.length - idx
}))
// for display, show from lowest -> highest points
.sort((a, b) => a.min - b.min);
const table = containerEl.createEl('table', { cls: 'cpwn-level-table' });
const headerRow = table.createEl('tr');
headerRow.createEl('th', { text: 'Level' });
headerRow.createEl('th', { text: 'Name' });
headerRow.createEl('th', { text: 'Icon' });
headerRow.createEl('th', { text: 'Points needed' });
for (const lvl of levels) {
const row = table.createEl('tr');
row.createEl('td', { text: String(lvl.level) });
row.createEl('td', { text: lvl.title });
const iconCell = row.createEl('td');
const iconDiv = iconCell.createDiv({ cls: 'cpwn-level-icon' });
setIcon(iconDiv, lvl.icon);
row.createEl('td', { text: lvl.min.toLocaleString() });
}
containerEl.createEl('div', { cls: 'cpwn-setting-spacer' });
// --- Reset Scoring Cache ---
new Setting(containerEl)
.setName("Reset scoring cache")
.setDesc("Clears cached daily scores and recomputes them from your tasks and notes. Vacation history is kept, but past totals may change.")
.addButton(btn => btn
.setButtonText("Reset cache")
.setWarning()
.onClick(async () => {
if (!confirm("Are you sure you want to reset the scoring cache? This will delete all cached daily scores and rebuild them from your tasks and notes.")) {
return;
}
this.plugin.settings.goalScoreHistory = {};
await this.plugin.saveData(this.plugin.settings);
new Notice("Goal scoring cache cleared. It will be rebuilt as you view dashboards.", 4000);
this.triggerDashboardRefresh();
}));
// --- Reset Vacation History ---
new Setting(containerEl)
.setName("Reset vacation history")
.setDesc("Clears stored vacation days for all goals. After resetting the scoring cache, past days will be recomputed without vacation applied.")
.addButton(btn => btn
.setButtonText("Reset vacation days")
.setWarning()
.onClick(async () => {
if (!confirm("Are you sure you want to reset all vacation history? All days will be treated as normal working days on the next rebuild.")) {
return;
}
this.plugin.settings.vacationHistory = {};
await this.plugin.saveData(this.plugin.settings);
new Notice("Vacation history cleared. Reset the scoring cache if you want past days recomputed without vacation.", 5000);
}));
}
getGoalDisplayName(goal) {
// If the user named it "Tasks completed" (default), rename it to "Core: Tasks"
// Or check by type if you want to force it
if (goal.type === 'task-count') return "Task count";
if (goal.type === 'word-count') return "Words count";
if (goal.type === 'note-created') return "Create note";
// Fallback for custom names
return goal.type;
}
renderGoalItem(goalsList, goal, index, isCore) {
const goalEl = goalsList.createDiv({ cls: 'cpwn-goal-item setting-item' });
goalEl.setAttribute('draggable', isCore ? 'false' : 'true');
goalEl.dataset.index = index;
// Drag handle (disabled for core)
const handle = goalEl.createDiv({ cls: 'cpwn-pm-drag-handle' });
setIcon(handle, 'grip-vertical');
handle.style.marginRight = '10px';
handle.style.cursor = isCore ? 'not-allowed' : 'grab';
if (isCore) handle.style.opacity = '0.5';
const info = goalEl.createDiv({ cls: 'setting-item-info' });
info.createEl('div', { text: goal.name, cls: 'setting-item-name' });
info.createEl('div', {
text: `${this.getGoalDisplayName(goal)} | Target: ${goal.target} | ${goal.points} pts`,
cls: 'setting-item-description'
});
const controls = goalEl.createDiv({ cls: 'setting-item-control' });
// EDIT (always)
new Setting(controls)
.addButton(btn => btn
.setIcon('pencil')
.onClick(() => {
new GoalEditModal(this.app, goal, async (updatedGoal) => {
this.plugin.settings.goals[index] = updatedGoal;
await this.saveAndUpdate();
this.renderGoalsSettings();
}).open();
}));
// TOGGLE (always)
new Setting(controls)
.addToggle(toggle => {
toggle.setValue(!!goal.enabled)
.onChange(async value => {
goal.enabled = value;
await this.saveAndUpdate();
this.renderGoalsSettings();
});
});
// DELETE (only for custom)
if (!isCore) {
new Setting(controls)
.addButton(btn => btn
.setIcon('trash')
.setWarning()
.onClick(async () => {
if (confirm(`Delete goal "${goal.name}"?`)) {
this.plugin.settings.goals.splice(index, 1);
await this.saveAndUpdate();
this.renderGoalsSettings();
}
}));
}
}
// Helper method to handle Drag & Drop logic
enableGoalSorting(container) {
let draggedItem = null;
container.addEventListener('dragstart', (e) => {
draggedItem = e.target.closest('.cpwn-goal-item');
if (draggedItem) {
draggedItem.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
// Firefox requires data to be set
e.dataTransfer.setData('text/plain', draggedItem.dataset.index);
}
});
container.addEventListener('dragend', async (e) => {
if (draggedItem) {
draggedItem.classList.remove('dragging');
// Update settings based on DOM order
const newOrder = [];
const children = Array.from(container.children);
children.forEach(child => {
const oldIndex = parseInt(child.dataset.index);
if (!isNaN(oldIndex) && this.plugin.settings.goals[oldIndex]) {
newOrder.push(this.plugin.settings.goals[oldIndex]);
}
});
this.plugin.settings.goals = newOrder;
await this.saveAndUpdate();
// Re-render to update indices
this.renderGoalsSettings();
}
draggedItem = null;
});
container.addEventListener('dragover', (e) => {
e.preventDefault();
const afterElement = this.getDragAfterElement(container, e.clientY);
if (draggedItem) {
if (afterElement == null) {
container.appendChild(draggedItem);
} else {
container.insertBefore(draggedItem, afterElement);
}
}
});
}
// Reusing your existing helper, or defining it here if it's private
getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.cpwn-goal-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
renderGeneralDashboardSettings(container) {
container.empty();

View file

@ -1,5 +1,5 @@
// src/utils/dateUtils.js
import moment from 'moment';
import { moment } from 'obsidian';
/**
* Calculates the custom period and week number based on a given start date.

View file

@ -10,10 +10,13 @@ export class TagSuggest extends AbstractInputSuggest {
this.inputEl = inputEl;
this.view = view;
this.allTags = new Set();
this.refreshAllTags();
}
getSuggestions(query) {
this.refreshAllTags();
//this.refreshAllTags();
// If the query is empty, show all tags, sorted alphabetically.
if (!query) {

File diff suppressed because it is too large Load diff

View file

@ -219,7 +219,7 @@ export class CssChartRenderer {
// Add the correct class to the main container. This is what the CSS will use.
if (showLegend) {
this.container.addClass(`cpwn-legend-${legendPosition}`);
console.log(legendPosition);
console.log(legendPosition);
} else {
// This class is for when the legend is off (e.g., mini/small widgets)
this.container.addClass('is-pie-only');
@ -900,4 +900,212 @@ export class CssChartRenderer {
this.renderLegend(chartContainer, datasets);
}
}
/**
* Renders a bespoke cumulative area chart for Goal Momentum.
* Strictly follows Obsidian UI guidelines (CSS classes, textContent).
*/
renderWeeklyGoalMomentum(chartData, options = {}) {
this.cleanup();
const chartContainer = this.container.createDiv({ cls: 'cpwn-pm-css-chart-container' });
const svgNs = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNs, "svg");
svg.setAttribute('viewBox', '0 0 300 135');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
chartContainer.appendChild(svg);
const dataset = chartData.datasets[0];
if (!dataset || dataset.data.length < 2) {
chartContainer.createDiv({ text: "Not enough data", cls: "cpwn-muted-text" });
return;
}
const rawData = dataset.data;
const minVal = Math.min(...rawData);
const maxVal = Math.max(...rawData);
const range = (maxVal - minVal) === 0 ? 10 : (maxVal - minVal);
const padding = { top: 15, right: 10, bottom: 10, left: 10 };
const width = 300 - padding.left - padding.right;
const height = 120 - padding.top - padding.bottom;
// Map values to coordinates
const points = rawData.map((val, i) => {
const x = padding.left + (i / (rawData.length - 1)) * width;
// Normalize Y: Lowest value sits at bottom (y = height + top)
const normalizedVal = val - minVal;
const y = padding.top + height - (normalizedVal / range) * height;
return { x, y, val };
});
// 1. Grid Lines (Stop at the dot)
points.forEach((p) => {
const gridLine = document.createElementNS(svgNs, 'line');
gridLine.classList.add('cpwn-chart-grid-line');
gridLine.setAttribute('x1', p.x);
gridLine.setAttribute('y1', p.y); // Start EXACTLY at the dot
gridLine.setAttribute('x2', p.x);
gridLine.setAttribute('y2', padding.top + height); // Go down to bottom axis
gridLine.setAttribute('stroke-opacity', '0.3'); // Subtle opacity via attribute
svg.appendChild(gridLine);
});
// 2. Gradient Definition
const defs = document.createElementNS(svgNs, 'defs');
const gradientId = 'goalMomentumGradient-' + Math.random().toString(36).substr(2, 9);
const gradient = document.createElementNS(svgNs, 'linearGradient');
gradient.setAttribute('id', gradientId);
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '0%');
gradient.setAttribute('y2', '100%');
const stop1 = document.createElementNS(svgNs, 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', 'var(--interactive-accent)');
stop1.setAttribute('stop-opacity', '0.5');
const stop2 = document.createElementNS(svgNs, 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', 'var(--interactive-accent)');
stop2.setAttribute('stop-opacity', '0.05');
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
svg.appendChild(defs);
// 3. Draw Paths
const lineD = points.map((p, i) => (i === 0 ? `M ${p.x},${p.y}` : `L ${p.x},${p.y}`)).join(' ');
const areaD = `${lineD} L ${points[points.length - 1].x},${padding.top + height} L ${points[0].x},${padding.top + height} Z`;
const areaPath = document.createElementNS(svgNs, 'path');
areaPath.classList.add('cpwn-chart-area-fill');
areaPath.setAttribute('d', areaD);
areaPath.setAttribute('fill', `url(#${gradientId})`);
svg.appendChild(areaPath);
const linePath = document.createElementNS(svgNs, 'path');
linePath.classList.add('cpwn-chart-line-stroke');
linePath.setAttribute('d', lineD);
svg.appendChild(linePath);
// 4. Dots & Hit Targets
points.forEach((p, i) => {
const circle = document.createElementNS(svgNs, 'circle');
circle.classList.add('cpwn-chart-dot');
circle.setAttribute('cx', p.x);
circle.setAttribute('cy', p.y);
circle.setAttribute('r', '3.5');
svg.appendChild(circle);
// Invisible Hit Target
const hitTarget = document.createElementNS(svgNs, 'rect');
hitTarget.classList.add('cpwn-chart-hit-target');
hitTarget.setAttribute('x', p.x - 10);
hitTarget.setAttribute('y', padding.top);
hitTarget.setAttribute('width', 20);
hitTarget.setAttribute('height', height);
// Tooltip Events (Using Arrow Functions to fix 'this' context)
hitTarget.addEventListener('mouseenter', (e) => {
const label = chartData.labels[i];
this.showTooltip(e, `${label}: ${p.val} pts`);
circle.setAttribute('r', '5.5'); // Highlight effect
});
hitTarget.addEventListener('mousemove', (e) => {
this.moveTooltip(e);
});
hitTarget.addEventListener('mouseleave', () => {
this.hideTooltip();
circle.setAttribute('r', '3.5'); // Reset
});
svg.appendChild(hitTarget);
});
const labelClass = options.labelClass || 'cpwn-axis-label';
// 5. X-Axis Labels
chartData.labels.forEach((label, i) => {
const text = document.createElementNS(svgNs, 'text');
text.classList.add(labelClass);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('x', points[i].x);
text.setAttribute('y', 130);
text.textContent = label; // Use textContent
svg.appendChild(text);
});
}
showTooltip(e, text) {
// 1. Create tooltip element if it doesn't exist yet
if (!this.tooltipEl) {
this.tooltipEl = document.body.createDiv({ cls: 'cpwn-chart-tooltip' });
// Safety: Ensure it doesn't block mouse events for the chart
this.tooltipEl.style.pointerEvents = 'none';
this.tooltipEl.style.position = 'absolute';
this.tooltipEl.style.zIndex = '9999'; // Ensure it sits on top
}
// 2. Set the text content
this.tooltipEl.setText(text);
// 3. Position and show it
this.moveTooltip(e);
this.tooltipEl.style.display = 'block';
}
/**
* Updates the tooltip position to follow the mouse.
* Adds smart positioning to prevent overflow on the right edge.
* @param {MouseEvent} e - The mouse movement event.
*/
moveTooltip(e) {
if (this.tooltipEl) {
const tooltipRect = this.tooltipEl.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const offset = 15; // Base offset from cursor
let left = e.pageX + offset;
let top = e.pageY + offset;
// Check for right overflow
if (left + tooltipRect.width > viewportWidth - 20) { // 20px buffer
// Flip to left side of cursor
left = e.pageX - tooltipRect.width - offset;
}
// Optional: Check for bottom overflow (flip up)
const viewportHeight = window.innerHeight;
if (top + tooltipRect.height > viewportHeight - 20) {
top = e.pageY - tooltipRect.height - offset;
}
this.tooltipEl.style.left = `${left}px`;
this.tooltipEl.style.top = `${top}px`;
}
}
/**
* Hides the tooltip from view.
*/
hideTooltip() {
if (this.tooltipEl) {
this.tooltipEl.style.display = 'none';
}
}
}

View file

@ -0,0 +1,48 @@
import { GoalTracker } from '../logic/GoalTracker';
import { moment } from 'obsidian';
export class GoalDataAggregator {
static async getTodaysGoals(app, settings, allTasks) {
const tracker = new GoalTracker(app, settings);
const score = await tracker.calculateDailyScore(moment(), allTasks);
return score.results; // [{ name: 'Journal', isMet: true, ... }]
}
static async getPointsHistory(app, settings, allTasks) {
const tracker = new GoalTracker(app, settings);
const labels = [];
const data = []; // This will hold the ACTUAL cumulative totals
// 1. Calculate "Baseline" Total (Score from deep past up to 7 days ago)
let runningTotal = 0;
const startOfHistory = moment().subtract(30, 'days');
const startOfChart = moment().subtract(6, 'days');
// Calculate baseline (simplified loop for performance)
for (let d = startOfHistory.clone(); d.isBefore(startOfChart, 'day'); d.add(1, 'day')) {
const score = await tracker.calculateDailyScore(d, allTasks);
runningTotal += score.totalPoints;
}
// 2. Calculate Visible 7 Days
for (let i = 6; i >= 0; i--) {
const date = moment().subtract(i, 'days');
labels.push(date.format('ddd'));
const score = await tracker.calculateDailyScore(date, allTasks);
runningTotal += score.totalPoints;
data.push(runningTotal);
}
return {
labels,
datasets: [{
label: 'Total Points',
data: data // [150, 160, 165...]
}]
};
}
}

View file

@ -1,4 +1,6 @@
// src/widgets/TaskDataAggregator.js
import { moment } from 'obsidian';
export class TaskDataAggregator {
/**
* Filters and groups tasks based on a widget configuration.

View file

@ -3109,6 +3109,11 @@ body.is-mobile .cpwn-tabs-content-wrapper {
fill: var(--text-muted);
}
.cpwn-pm-line-chart .cpwn-small-axis-label {
font-size: 14px;
fill: var(--text-muted);
}
.cpwn-pm-line-chart .line-path {
fill: none;
stroke-width: 3.5; /* Slightly thicker line */
@ -3485,21 +3490,21 @@ body.is-mobile .cpwn-tabs-content-wrapper {
/* Pie Chart Size Differentiation */
.cpwn-pm-widget-medium .cpwn-pm-widget-pie {
min-height: 175px; /* Default for medium */
max-height: 150px; /* Default for medium */
}
.cpwn-pm-widget-medium .cpwn-pm-widget-pie .cpwn-pm-pie-chart {
min-height: 150px;
/*min-height: 150px;*/
max-height: 175px;
}
.cpwn-pm-widget-large .cpwn-pm-widget-pie {
min-height: 260px; /* Taller for large */
max-height: 260px; /* Taller for large */
}
/* Make sure the pie itself scales appropriately */
.cpwn-pm-widget-large .cpwn-pm-widget-pie .cpwn-pm-pie-chart {
min-height: 220px;
max-height: 220px;
/* min-height: 220px;*/
max-height: 260px;
}
/* -- LAYOUTS -- */
@ -4028,4 +4033,510 @@ body.is-mobile .cpwn-tabs-content-wrapper {
.cpwn-fade-visible {
opacity: 1;
}
}
/* Chart Container */
.cpwn-pm-css-chart-container {
position: relative;
width: 100%;
height: 100%;
}
.cpwn-pm-css-chart-container svg {
overflow: visible;
width: 100%;
font-family: var(--font-ui);
}
/* Grid Lines */
.cpwn-chart-grid-line {
stroke: var(--background-modifier-border);
stroke-width: 1px;
}
.cpwn-chart-line-stroke {
fill: none;
stroke: var(--interactive-accent);
stroke-width: 2px;
}
/* Data Points */
.cpwn-chart-dot {
fill: var(--interactive-accent);
stroke: var(--background-primary);
stroke-width: 1px;
transition: r 0.2s ease;
}
/* Hit Targets (Invisible) */
.cpwn-chart-hit-target {
fill: transparent;
cursor: pointer;
}
/* Axis Labels */
.cpwn-axis-label {
font-size: 10px;
fill: var(--text-muted);
text-anchor: middle;
}
/* Tooltip */
.cpwn-chart-tooltip {
position: absolute;
z-index: var(--layer-tooltip);
pointer-events: none;
padding: 4px 8px;
background-color: var(--background-modifier-message);
color: var(--text-normal);
border-radius: var(--radius-s);
font-size: var(--font-ui-smaller);
box-shadow: var(--shadow-s);
display: none;
}
/* Goal Row Layout */
.cpwn-goal-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
padding: 2px 0;
}
/* Left Section (Icon + Text) */
.cpwn-goal-left {
display: flex;
align-items: center;
flex-grow: 1;
min-width: 0; /* Allows text truncation if needed */
}
/* Icon Styling */
.cpwn-goal-icon {
margin-right: 8px;
display: flex;
align-items: center;
}
.cpwn-goal-icon.is-completed {
color: var(--text-success);
}
.cpwn-goal-icon.is-incomplete {
color: var(--text-muted);
}
/* Text Container */
.cpwn-goal-text {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.cpwn-goal-title {
font-weight: var(--font-ui-small);
color: var(--text-normal);
}
.cpwn-goal-progress {
font-size: var(--font-ui-smaller);
/* cpwn-muted-text handles color, but we ensure size here */
}
/* Right Section (Points) */
.cpwn-goal-right {
white-space: nowrap;
margin-left: 8px;
}
.cpwn-goal-points {
font-weight: bold;
font-size: var(--font-ui-small);
color: var(--text-accent);
}
.cpwn-goal-list-left .cpwn-goal-row:first-child {
margin-top: 10px;
}
/* --- Daily Goals Widget Split Layout --- */
.cpwn-goal-split-container {
display: grid;
grid-template-columns: 1fr 80px; /* List takes available space, Level takes fixed 80px */
gap: 12px;
align-items: center;
}
/* Left List Styling */
.cpwn-goal-list-left {
display: flex;
flex-direction: column;
/*gap: 6px;*/
}
.cpwn-goal-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 0;
}
.cpwn-goal-left {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden; /* Prevent text overflow */
}
.cpwn-goal-title-row {
display: flex;
align-items: baseline;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cpwn-goal-title {
font-size: 0.95em;
color: var(--text-normal);
}
.cpwn-goal-count-inline {
font-size: 0.85em;
color: var(--text-muted);
}
.cpwn-goal-right {
margin-left: 8px;
white-space: nowrap;
}
.cpwn-goal-points {
font-weight: bold;
font-size: 0.9em;
}
.cpwn-points-pos { color: var(--text-accent); }
.cpwn-points-neg { color: var(--text-error); }
/* Right Level Styling */
.cpwn-goal-level-right {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.cpwn-level-circle-wrapper {
position: relative;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.cpwn-level-icon-inner {
position: absolute;
color: var(--text-normal);
display: flex;
align-items: center;
justify-content: center;
}
.cpwn-level-icon-inner svg {
width: 34px;
height: 34px;
}
.cpwn-level-title-text {
font-size: 0.75em;
color: var(--text-muted);
margin-top: 6px;
line-height: 1.1;
max-width: 80px;
}
.cpwn-level-circle-wrapper svg circle:nth-child(2) {
transition: stroke-dashoffset 0.5s ease;
/* Ensure the stroke color is visible */
stroke: var(--text-accent);
/* Ensure it's on top */
z-index: 2;
}
/* Goal progress tooltip, styled like a mini popup window */
.cpwn-goal-tooltip {
position: fixed;
z-index: 9999;
max-width: 360px;
padding: 10px 12px;
border-radius: 8px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
color: var(--text-normal);
font-size: 12px;
line-height: 1.4;
opacity: 0;
transition: opacity 0.15s ease-out;
pointer-events: none;
}
/* Optional: reuse popup font/spacing feel */
.cpwn-goal-tooltip-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
/* Wrapper for the mini ring */
.cpwn-goal-tooltip-ring-wrapper {
position: relative;
width: 28px;
height: 28px;
flex-shrink: 0;
}
.cpwn-goal-tooltip-icon {
display: flex;
align-items: center;
justify-content: center;
}
/* Performance state icon (color set inline from JS) */
.cpwn-goal-tooltip-icon-perf {
opacity: 0.9;
}
/* Level icon uses the progress accent colour */
.cpwn-goal-tooltip-icon-level {
/* Bigger icon in the popup */
width: 28px;
height: 28px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
/* Progress value (0100) comes from JS; default to 0 */
--cpwn-goal-progress: 0;
/* Outer ring: accent for [0, progress], dull border after that */
background:
conic-gradient(
var(--text-accent)
0deg
calc(var(--cpwn-goal-progress) * 3.6deg),
var(--background-modifier-border)
calc(var(--cpwn-goal-progress) * 3.6deg)
360deg
),
radial-gradient(
farthest-side,
var(--background-primary) 70%,
transparent 71%
);
/* Make sure the inner circle stays the background color, ring outside */
background-origin: border-box;
background-clip: border-box, padding-box;
}
/* Center the Lucide icon over the SVG ring */
.cpwn-goal-tooltip-ring-icon {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* Make the Lucide glyph itself clearly visible inside the ring */
.cpwn-goal-tooltip-ring-icon svg {
width: 14px;
height: 14px;
stroke: var(--text-normal);
color: var(--text-normal);
}
.cpwn-goal-tooltip-title {
font-weight: 600;
font-size: 12px;
}
.cpwn-goal-tooltip-body {
margin-top: 2px;
}
.cpwn-goal-tooltip-line-strong {
font-weight: 500;
margin-bottom: 2px;
}
.cpwn-goal-tooltip-line {
color: var(--text-muted);
margin-bottom: 2px;
}
.cpwn-goal-tooltip-spacer {
height: 4px;
}
.cpwn-goal-tooltip-spacer-large {
height: 8px; /* gap between “Nice and steady…” and “At this pace…” */
}
/* Weekday pill selector in GoalEditModal */
.cpwn-goal-days-picker {
display: inline-flex;
border-radius: 999px;
overflow: hidden; /* clip inner corners */
background-color: var(--background-modifier-border);
}
/* Each day cell: larger tap target */
.cpwn-goal-day {
padding: 6px 16px; /* bigger horizontally and vertically */
min-width: 28px; /* wider cells */
text-align: center;
font-size: 12px; /* larger letter */
letter-spacing: 0.06em;
cursor: pointer;
color: var(--text-muted);
background-color: transparent;
border-right: 1px solid rgba(0, 0, 0, 0.25);
}
/* Make the very first cell visually round on the left */
.cpwn-goal-day:first-child {
border-top-left-radius: 999px;
border-bottom-left-radius: 999px;
}
/* Make the very last cell visually round on the right */
.cpwn-goal-day:last-child {
border-top-right-radius: 999px;
border-bottom-right-radius: 999px;
border-right: none;
}
/* Active days: Obsidian accent, but keep corner rounding from above */
.cpwn-goal-day.is-active {
background-color: var(--interactive-accent);
color: var(--text-on-accent, #fff);
}
.cpwn-level-table {
width: 100%;
border-collapse: collapse;
margin-top: 6px;
font-size: 0.9em;
}
.cpwn-level-table th,
.cpwn-level-table td {
padding: 4px 6px;
}
.cpwn-level-table tr:nth-child(even) {
background-color: var(--background-secondary-alt);
}
.cpwn-level-icon svg {
width: 16px;
height: 16px;
}
.cpwn-goal-item .setting-item {
border-top: none;
border-bottom: none;
padding: 0; /* Remove default padding if it's too large */
}
/* Dim the entire row */
.cpwn-goal-item.cpwn-goal-inactive {
opacity: 0.5;
filter: grayscale(100%);
}
/* Style the "Rest" icon */
.cpwn-goal-item.cpwn-goal-inactive .cpwn-goal-icon {
color: var(--text-muted);
cursor: default;
}
/* Style the "OFF" badge */
.cpwn-goal-rest-badge {
font-size: 0.7em;
font-weight: bold;
color: var(--text-muted);
border: 1px solid var(--text-muted);
border-radius: 4px;
padding: 2px 6px;
margin-left: auto; /* Push to the right */
}
/* Condensed Daily Goal Widget Layout */
.cpwn-goal-split-container.condensed {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
}
.cpwn-goal-summary-large {
font-size: 2em;
font-weight: bold;
line-height: 1;
}
.cpwn-goal-summary-sub {
font-size: 0.8em;
color: var(--text-muted);
}
/* The larger font size for the condensed chart */
.cpwn-axis-label-large {
font-size: 16px; /* Larger font */
fill: var(--text-muted);
}
/* Vacation Text */
.cpwn-summary-vacation {
font-size: 0.9em;
color: var(--text-muted);
}
/* Large Numbers (e.g. "1/3") */
.cpwn-summary-large {
font-size: 2.2em;
font-weight: bold;
line-height: 1;
color: var(--text-normal);
}
.cpwn-summary-icon-rest {
display: flex;
justify-content: center; /* Center horizontally */
margin-top: 10px;
margin-bottom: 2px;
color: var(--text-muted);
}
.cpwn-summary-icon-rest svg {
width: 44px;
height: 44px;
}
/* Subtext (e.g. "goals met" or "2 off today") */
.cpwn-summary-sub {
padding-top: 10px;
font-size: 0.6em;
color: var(--text-muted);
}