diff --git a/package-lock.json b/package-lock.json index 4fedc79..e7a2f9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,10 +15,12 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@vitest/ui": "^4.0.8", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vitest": "^4.0.8" } }, "node_modules/@ampproject/remapping": { @@ -324,6 +326,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -341,6 +360,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -358,6 +394,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -462,9 +515,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "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" }, @@ -487,9 +540,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", - "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz", + "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==", "cpu": [ "arm" ], @@ -501,9 +554,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", - "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz", + "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==", "cpu": [ "arm64" ], @@ -515,9 +568,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", - "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz", + "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==", "cpu": [ "arm64" ], @@ -529,9 +582,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", - "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz", + "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==", "cpu": [ "x64" ], @@ -543,9 +596,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", - "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz", + "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==", "cpu": [ "arm64" ], @@ -557,9 +610,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", - "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz", + "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==", "cpu": [ "x64" ], @@ -571,9 +624,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", - "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz", + "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==", "cpu": [ "arm" ], @@ -585,9 +638,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", - "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz", + "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==", "cpu": [ "arm" ], @@ -599,9 +652,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", - "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz", + "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==", "cpu": [ "arm64" ], @@ -613,9 +666,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", - "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz", + "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==", "cpu": [ "arm64" ], @@ -626,10 +679,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", - "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz", + "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz", + "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==", "cpu": [ "ppc64" ], @@ -641,9 +708,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", - "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz", + "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz", + "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==", "cpu": [ "riscv64" ], @@ -655,9 +736,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", - "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz", + "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==", "cpu": [ "s390x" ], @@ -669,9 +750,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", - "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz", + "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==", "cpu": [ "x64" ], @@ -683,9 +764,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", - "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz", + "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==", "cpu": [ "x64" ], @@ -696,10 +777,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz", + "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", - "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz", + "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==", "cpu": [ "arm64" ], @@ -711,9 +806,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", - "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz", + "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==", "cpu": [ "ia32" ], @@ -724,10 +819,10 @@ "win32" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", - "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz", + "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==", "cpu": [ "x64" ], @@ -738,6 +833,27 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz", + "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@sveltejs/adapter-auto": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", @@ -824,6 +940,17 @@ "vite": "^5.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -831,13 +958,126 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, + "node_modules/@types/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/@vitest/expect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", + "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", + "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", + "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", + "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", + "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.8.tgz", + "integrity": "sha512-F9jI5rSstNknPlTlPN2gcc4gpbaagowuRzw/OJzl368dvPun668Q182S8Q8P9PITgGCl5LAKXpzuue106eM4wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.8", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.8" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", + "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.8", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -877,6 +1117,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astronomia": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz", @@ -908,6 +1158,16 @@ "node": ">=12.0.0" } }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", @@ -1002,9 +1262,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1041,6 +1301,13 @@ "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", "license": "MIT" }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -1098,12 +1365,35 @@ "@types/estree": "^1.0.1" } }, - "node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -1113,6 +1403,20 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1217,13 +1521,13 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/moment": { @@ -1275,9 +1579,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -1293,6 +1597,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1300,10 +1611,23 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1321,8 +1645,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -1353,13 +1677,13 @@ } }, "node_modules/rollup": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", - "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz", + "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -1369,24 +1693,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.25.0", - "@rollup/rollup-android-arm64": "4.25.0", - "@rollup/rollup-darwin-arm64": "4.25.0", - "@rollup/rollup-darwin-x64": "4.25.0", - "@rollup/rollup-freebsd-arm64": "4.25.0", - "@rollup/rollup-freebsd-x64": "4.25.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", - "@rollup/rollup-linux-arm-musleabihf": "4.25.0", - "@rollup/rollup-linux-arm64-gnu": "4.25.0", - "@rollup/rollup-linux-arm64-musl": "4.25.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", - "@rollup/rollup-linux-riscv64-gnu": "4.25.0", - "@rollup/rollup-linux-s390x-gnu": "4.25.0", - "@rollup/rollup-linux-x64-gnu": "4.25.0", - "@rollup/rollup-linux-x64-musl": "4.25.0", - "@rollup/rollup-win32-arm64-msvc": "4.25.0", - "@rollup/rollup-win32-ia32-msvc": "4.25.0", - "@rollup/rollup-win32-x64-msvc": "4.25.0", + "@rollup/rollup-android-arm-eabi": "4.53.1", + "@rollup/rollup-android-arm64": "4.53.1", + "@rollup/rollup-darwin-arm64": "4.53.1", + "@rollup/rollup-darwin-x64": "4.53.1", + "@rollup/rollup-freebsd-arm64": "4.53.1", + "@rollup/rollup-freebsd-x64": "4.53.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.1", + "@rollup/rollup-linux-arm-musleabihf": "4.53.1", + "@rollup/rollup-linux-arm64-gnu": "4.53.1", + "@rollup/rollup-linux-arm64-musl": "4.53.1", + "@rollup/rollup-linux-loong64-gnu": "4.53.1", + "@rollup/rollup-linux-ppc64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-musl": "4.53.1", + "@rollup/rollup-linux-s390x-gnu": "4.53.1", + "@rollup/rollup-linux-x64-gnu": "4.53.1", + "@rollup/rollup-linux-x64-musl": "4.53.1", + "@rollup/rollup-openharmony-arm64": "4.53.1", + "@rollup/rollup-win32-arm64-msvc": "4.53.1", + "@rollup/rollup-win32-ia32-msvc": "4.53.1", + "@rollup/rollup-win32-x64-gnu": "4.53.1", + "@rollup/rollup-win32-x64-msvc": "4.53.1", "fsevents": "~2.3.2" } }, @@ -1410,10 +1738,17 @@ "dev": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1435,6 +1770,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/svelte": { "version": "5.1.13", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.13.tgz", @@ -1495,6 +1844,47 @@ "globrex": "^0.1.2" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -1598,6 +1988,636 @@ } } }, + "node_modules/vitest": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", + "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.8", + "@vitest/mocker": "4.0.8", + "@vitest/pretty-format": "4.0.8", + "@vitest/runner": "4.0.8", + "@vitest/snapshot": "4.0.8", + "@vitest/spy": "4.0.8", + "@vitest/utils": "4.0.8", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.8", + "@vitest/browser-preview": "4.0.8", + "@vitest/browser-webdriverio": "4.0.8", + "@vitest/ui": "4.0.8", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", + "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/package.json b/package.json index 532b413..286c316 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,21 @@ "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@vitest/ui": "^4.0.8", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vitest": "^4.0.8" }, "dependencies": { "date-holidays": "^3.23.12", diff --git a/src/lib/CalendarMonth.svelte b/src/lib/CalendarMonth.svelte index 40e5262..326dc9a 100644 --- a/src/lib/CalendarMonth.svelte +++ b/src/lib/CalendarMonth.svelte @@ -8,6 +8,8 @@ export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>; export let selectedCountryCode: string; export let weekendDays: number[] = [6, 0]; + export let startDate: Date = new Date(year, 0, 1); + export let isActive: boolean = true; // Function to determine the first day of the week based on locale function getFirstDayOfWeek(locale: string): number { @@ -84,12 +86,19 @@ return weekendDays.includes(date.getDay()); } + function isPastDate(day: number): boolean { + const date = new Date(year, month, day); + // Normalize startDate to current year for comparison + const startDateInYear = new Date(year, startDate.getMonth(), startDate.getDate()); + return date < startDateInYear; + } + const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; $: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek)); -
+
{new Date(year, month).toLocaleString('default', { month: 'long' })}
{#each orderedDayInitials as dayInitial} @@ -101,7 +110,8 @@ {/each} {#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day} {@const holiday = getHoliday(day)} -
+ {@const pastDate = isPastDate(day)} +
{day} {#if holiday} @@ -137,6 +147,14 @@ color: #c5c6c7; font-size: 0.6em; } + + .excluded-month .month-name { + color: #666; + } + + .excluded-month .day-initial { + color: #666; + } .day { aspect-ratio: 1; text-align: center; @@ -202,4 +220,12 @@ text-decoration: line-through; opacity: 0.5; } + + .past-date { + opacity: 0.4; + } + + .past-date span { + text-decoration: line-through; + } \ No newline at end of file diff --git a/src/lib/holidayUtils.test.ts b/src/lib/holidayUtils.test.ts new file mode 100644 index 0000000..b114603 --- /dev/null +++ b/src/lib/holidayUtils.test.ts @@ -0,0 +1,781 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from './holidayUtils'; + +// Test constants +const TEST_YEAR = 2024; +const DEFAULT_WEEKENDS = [0, 6]; // Sunday, Saturday +const CUSTOM_WEEKENDS = [5, 6]; // Friday, Saturday + +// Mock browser APIs +const mockNavigator = { + languages: ['en', 'en-US'] +}; + +const mockIntlDateTimeFormat = vi.fn(() => ({ + resolvedOptions: () => ({ timeZone: 'America/New_York' }) +})); + +describe('holidayUtils', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal('navigator', mockNavigator); + vi.stubGlobal('Intl', { + ...Intl, + DateTimeFormat: mockIntlDateTimeFormat + }); + }); + + describe('getHolidaysForYear', () => { + it('should return holidays for a given year and country', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + expect(holidays).toBeDefined(); + expect(Array.isArray(holidays)).toBe(true); + expect(holidays.length).toBeGreaterThan(0); + }); + + it('should filter only public holidays', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + holidays.forEach(holiday => { + expect(holiday).toHaveProperty('date'); + expect(holiday).toHaveProperty('name'); + expect(holiday.date).toBeInstanceOf(Date); + expect(typeof holiday.name).toBe('string'); + }); + }); + + it('should handle state codes', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR, 'CA'); + expect(holidays).toBeDefined(); + expect(Array.isArray(holidays)).toBe(true); + }); + + it('should return holidays sorted by date', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + for (let i = 1; i < holidays.length; i++) { + const prev = holidays[i - 1].date.getTime(); + const curr = holidays[i].date.getTime(); + expect(curr).toBeGreaterThanOrEqual(prev); + } + }); + + it('should handle different countries', () => { + const usHolidays = getHolidaysForYear('US', TEST_YEAR); + const gbHolidays = getHolidaysForYear('GB', TEST_YEAR); + + expect(usHolidays.length).toBeGreaterThan(0); + expect(gbHolidays.length).toBeGreaterThan(0); + expect(usHolidays.length).not.toBe(gbHolidays.length); + }); + + it('should expand multi-day holidays correctly', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + const dateKeys = new Set(holidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + expect(holidays.length).toBeGreaterThanOrEqual(dateKeys.size); + }); + + it('should sort holidays by date first, then by name', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + for (let i = 1; i < holidays.length; i++) { + const prev = holidays[i - 1]; + const curr = holidays[i]; + const prevTime = prev.date.getTime(); + const currTime = curr.date.getTime(); + + if (prevTime === currTime) { + expect(curr.name.localeCompare(prev.name)).toBeGreaterThanOrEqual(0); + } else { + expect(currTime).toBeGreaterThan(prevTime); + } + } + }); + }); + + describe('optimizeDaysOff', () => { + const mockHolidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' }, + ]; + + describe('basic functionality', () => { + it('should return an array of dates', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + result.forEach(date => { + expect(date).toBeInstanceOf(Date); + }); + }); + + it('should return at most the requested number of days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5); + expect(result.length).toBeLessThanOrEqual(5); + }); + + it('should handle zero days off', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 0); + expect(result).toEqual([]); + }); + + it('should handle more days off than available gaps', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 1000); + expect(result.length).toBeGreaterThan(0); + expect(result.length).toBeLessThan(1000); + }); + }); + + describe('exclusion rules', () => { + it('should not include weekends in optimized days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should not include holidays in optimized days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 10); + const holidayKeys = new Set(mockHolidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + result.forEach(date => { + const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; + expect(holidayKeys.has(dateKey)).toBe(false); + }); + }); + + it('should not select days that are already holidays or weekends', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + const holidaySet = new Set(holidays.map(h => + `${h.date.getFullYear()}-${h.date.getMonth()}-${h.date.getDate()}` + )); + result.forEach(date => { + const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; + expect(holidaySet.has(dateKey)).toBe(false); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + }); + + describe('parameters', () => { + it('should respect startDate parameter', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + + it('should handle custom weekend days', () => { + const result = optimizeDaysOff(mockHolidays, TEST_YEAR, 5, CUSTOM_WEEKENDS); + result.forEach(date => { + expect(CUSTOM_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should filter holidays by year and startDate', () => { + const holidays = [ + { date: new Date(2023, 11, 31), name: 'Old Year' }, + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 5, 15), name: 'Mid Year' }, + ]; + const startDate = new Date(TEST_YEAR, 2, 1); + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('gap finding and prioritization', () => { + it('should only find gaps of MAX_GAP_LENGTH (5) days or less', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed' }, + { date: new Date(TEST_YEAR, 0, 10), name: 'Wed' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + expect(Array.isArray(result)).toBe(true); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should find and fill gaps of 1-5 days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed Holiday' }, + { date: new Date(TEST_YEAR, 0, 11), name: 'Thu Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + expect(result.length).toBeGreaterThan(0); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should prioritize gaps that create longer consecutive periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thursday Holiday' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Monday Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBe(1); + }); + + it('should prioritize smaller gaps when they create longer chains', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu Holiday' }, + { date: new Date(TEST_YEAR, 0, 9), name: 'Tue Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBe(1); + expect(result[0].getDate()).toBe(5); + }); + + it('should handle multiple gaps and select most efficient ones first', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 3); + expect(result.length).toBeLessThanOrEqual(3); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should handle backward vs forward chain calculation', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 5), name: 'Friday Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(Array.isArray(result)).toBe(true); + }); + + it('should optimize to create longer consecutive periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 1); + expect(result.length).toBeGreaterThan(0); + }); + }); + + describe('edge cases', () => { + it('should handle partial gap filling when daysOff is less than gap length', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 2); + expect(result.length).toBe(2); + result.forEach(date => { + expect(date.getDate()).toBeGreaterThanOrEqual(2); + expect(date.getDate()).toBeLessThanOrEqual(6); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should handle multiple gaps when daysOff exceeds single gap capacity', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 3), name: 'Wed' }, + { date: new Date(TEST_YEAR, 0, 5), name: 'Fri' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 3); + expect(result.length).toBeGreaterThanOrEqual(2); + expect(result.length).toBeLessThanOrEqual(3); + }); + + it('should handle optimization with no available gaps', () => { + const holidays = Array.from({ length: 365 }, (_, i) => { + const date = new Date(TEST_YEAR, 0, 1); + date.setDate(date.getDate() + i); + if (date.getDay() !== 0 && date.getDay() !== 6) { + return { date, name: `Holiday ${i}` }; + } + return null; + }).filter(Boolean) as Array<{ date: Date; name: string }>; + + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(result).toEqual([]); + }); + + it('should handle gaps at the start of the year', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 5), name: 'Holiday' }, + ]; + const startDate = new Date(TEST_YEAR, 0, 1); + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle gaps at the end of the year', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + result.forEach(date => { + expect(date.getFullYear()).toBe(TEST_YEAR); + expect(date.getMonth()).toBeLessThanOrEqual(11); + }); + }); + + it('should handle gaps that span year boundaries correctly', () => { + const startDate = new Date(TEST_YEAR, 11, 20); + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getFullYear()).toBe(TEST_YEAR); + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + }); + }); + + describe('calculateConsecutiveDaysOff', () => { + const mockHolidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 0, 15), name: 'Holiday' }, + ]; + + describe('basic functionality', () => { + it('should return an array of periods', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + + it('should calculate periods with correct structure', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period).toHaveProperty('startDate'); + expect(period).toHaveProperty('endDate'); + expect(period).toHaveProperty('totalDays'); + expect(period).toHaveProperty('usedDaysOff'); + expect(period.startDate).toBeInstanceOf(Date); + expect(period.endDate).toBeInstanceOf(Date); + expect(typeof period.totalDays).toBe('number'); + expect(typeof period.usedDaysOff).toBe('number'); + }); + }); + + it('should include holidays in consecutive periods', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + const hasPeriodWithHoliday = result.some(period => { + const holidayDate = mockHolidays[0].date; + return period.startDate <= holidayDate && period.endDate >= holidayDate; + }); + expect(hasPeriodWithHoliday).toBe(true); + }); + }); + + describe('calculations', () => { + it('should calculate totalDays correctly', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + const calculatedDays = Math.round( + (period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24) + ) + 1; + expect(period.totalDays).toBe(calculatedDays); + }); + }); + + it('should count usedDaysOff correctly', () => { + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + const daysInPeriod = optimizedDays.filter(day => + day >= period.startDate && day <= period.endDate + ).length; + expect(period.usedDaysOff).toBeLessThanOrEqual(daysInPeriod); + }); + }); + + it('should correctly count usedDaysOff in periods', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const periodWithOptimized = result.find(period => + optimizedDays.some(day => day >= period.startDate && day <= period.endDate) + ); + if (periodWithOptimized) { + const daysInPeriod = optimizedDays.filter(day => + day >= periodWithOptimized.startDate && day <= periodWithOptimized.endDate + ).length; + expect(periodWithOptimized.usedDaysOff).toBe(daysInPeriod); + } + }); + }); + + describe('validation rules', () => { + it('should not include periods that are only weekends', () => { + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + let allWeekends = true; + for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek !== 0 && dayOfWeek !== 6) { + allWeekends = false; + break; + } + } + expect(allWeekends).toBe(false); + }); + }); + + it('should exclude single-day periods', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + }); + }); + + it('should handle groups that are only weekends correctly', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const optimizedDays: Date[] = []; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + let hasNonWeekend = false; + for (let d = new Date(period.startDate); d <= period.endDate; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + if (dayOfWeek !== 0 && dayOfWeek !== 6) { + hasNonWeekend = true; + break; + } + } + expect(hasNonWeekend).toBe(true); + }); + }); + }); + + describe('parameters', () => { + it('should respect startDate parameter', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const optimizedDays = [new Date(TEST_YEAR, 5, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate); + result.forEach(period => { + expect(period.startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + }); + }); + + it('should handle custom weekend days', () => { + const optimizedDays = [new Date(TEST_YEAR, 0, 2)]; + const result = calculateConsecutiveDaysOff(mockHolidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle empty optimized days', () => { + const result = calculateConsecutiveDaysOff(mockHolidays, [], TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('edge cases', () => { + it('should handle periods spanning multiple months', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 31), name: 'End of Jan' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 1, 1)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle periods that start exactly at startDate', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const holidays = [ + { date: new Date(TEST_YEAR, 5, 1), name: 'Start Holiday' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 5, 3)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, DEFAULT_WEEKENDS, startDate); + if (result.length > 0) { + expect(result[0].startDate.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + } + }); + + it('should handle periods that end exactly at year end (Dec 31)', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 30), name: 'Dec 30' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 11, 31)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const periodAtYearEnd = result.find(period => + period.endDate.getMonth() === 11 && period.endDate.getDate() === 31 + ); + if (periodAtYearEnd) { + expect(periodAtYearEnd.endDate.getFullYear()).toBe(TEST_YEAR); + } + }); + + it('should correctly handle overlapping optimized days and holidays', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 3), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + const period = result.find(p => + p.startDate <= holidays[0].date && p.endDate >= optimizedDays[1] + ); + if (period) { + expect(period.usedDaysOff).toBe(2); + } + }); + + it('should handle consecutive periods separated by work days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Mon' }, + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + ]; + const optimizedDays = [ + new Date(TEST_YEAR, 0, 2), + new Date(TEST_YEAR, 0, 5), + ]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + expect(result.length).toBeGreaterThanOrEqual(1); + result.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + }); + }); + }); + }); + + describe('Integration tests', () => { + it('should work together: get holidays, optimize, and calculate periods', () => { + const holidays = getHolidaysForYear('US', TEST_YEAR); + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(holidays.length).toBeGreaterThan(0); + expect(optimizedDays.length).toBeLessThanOrEqual(10); + expect(Array.isArray(periods)).toBe(true); + + periods.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + expect(period.startDate <= period.endDate).toBe(true); + }); + }); + + it('should optimize efficiently to maximize consecutive days', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Holiday' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + if (periods.length > 0) { + const hasOptimizedDay = periods.some(period => + optimizedDays.some(day => + day >= period.startDate && day <= period.endDate + ) + ); + expect(hasOptimizedDay).toBe(true); + } + }); + + it('should handle edge case: all days are holidays or weekends', () => { + const holidays = Array.from({ length: 50 }, (_, i) => ({ + date: new Date(TEST_YEAR, 0, i + 1), + name: `Holiday ${i + 1}` + })); + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(Array.isArray(optimizedDays)).toBe(true); + expect(Array.isArray(periods)).toBe(true); + }); + }); + + describe('Edge cases and error handling', () => { + it('should handle year with no holidays gracefully', () => { + const result = optimizeDaysOff([], TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle invalid country codes gracefully', () => { + try { + const holidays = getHolidaysForYear('XX', TEST_YEAR); + expect(Array.isArray(holidays)).toBe(true); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('should handle dates at year boundaries', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 11, 31), name: 'New Year Eve' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle startDate at end of year', () => { + const startDate = new Date(TEST_YEAR, 11, 15); + const holidays = [ + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + expect(date.getFullYear()).toBe(TEST_YEAR); + }); + }); + + it('should handle holidays from previous year correctly', () => { + const holidays = [ + { date: new Date(2023, 11, 31), name: 'Old Year' }, + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle leap year correctly', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 1, 29), name: 'Leap Day' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('Private function behavior (tested indirectly)', () => { + it('should correctly identify weekend days', () => { + const holidays: Array<{ date: Date; name: string }> = []; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10); + result.forEach(date => { + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + + it('should correctly calculate days between dates', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Start' }, + ]; + const optimizedDays = [new Date(TEST_YEAR, 0, 5)]; + const result = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + result.forEach(period => { + const calculated = Math.round( + (period.endDate.getTime() - period.startDate.getTime()) / (1000 * 60 * 60 * 24) + ) + 1; + expect(period.totalDays).toBe(calculated); + }); + }); + + it('should generate consistent date keys', () => { + const date1 = new Date(TEST_YEAR, 0, 15); + const date2 = new Date(TEST_YEAR, 0, 15); + const holidays = [ + { date: date1, name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + const hasDate2 = result.some(d => + d.getFullYear() === date2.getFullYear() && + d.getMonth() === date2.getMonth() && + d.getDate() === date2.getDate() + ); + expect(hasDate2).toBe(false); + }); + + it('should correctly identify holidays using dateKey', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 15, 10, 30), name: 'Holiday' }, + ]; + const result = optimizeDaysOff(holidays, TEST_YEAR, 5); + const hasHolidayDate = result.some(d => + d.getFullYear() === TEST_YEAR && + d.getMonth() === 0 && + d.getDate() === 15 + ); + expect(hasHolidayDate).toBe(false); + }); + + it('should correctly get weekends for the year with startDate', () => { + const startDate = new Date(TEST_YEAR, 5, 1); + const holidays: Array<{ date: Date; name: string }> = []; + const result = optimizeDaysOff(holidays, TEST_YEAR, 10, DEFAULT_WEEKENDS, startDate); + result.forEach(date => { + expect(date.getTime()).toBeGreaterThanOrEqual(startDate.getTime()); + expect(DEFAULT_WEEKENDS).not.toContain(date.getDay()); + }); + }); + }); + + describe('Complex scenarios and real-world cases', () => { + it('should handle a typical year with multiple holidays and weekends', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'New Year' }, + { date: new Date(TEST_YEAR, 4, 27), name: 'Memorial Day' }, + { date: new Date(TEST_YEAR, 6, 4), name: 'Independence Day' }, + { date: new Date(TEST_YEAR, 8, 2), name: 'Labor Day' }, + { date: new Date(TEST_YEAR, 10, 28), name: 'Thanksgiving' }, + { date: new Date(TEST_YEAR, 11, 25), name: 'Christmas' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 10); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + expect(optimizedDays.length).toBeLessThanOrEqual(10); + expect(periods.length).toBeGreaterThan(0); + + periods.forEach(period => { + expect(period.totalDays).toBeGreaterThanOrEqual(2); + expect(period.startDate <= period.endDate).toBe(true); + expect(period.usedDaysOff).toBeGreaterThanOrEqual(0); + }); + }); + + it('should maximize consecutive days off efficiently', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 4), name: 'Thu' }, + { date: new Date(TEST_YEAR, 0, 8), name: 'Mon' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 1); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR); + + const periodWithOptimized = periods.find(p => + optimizedDays.some(day => day >= p.startDate && day <= p.endDate) + ); + expect(periodWithOptimized).toBeDefined(); + if (periodWithOptimized) { + expect(periodWithOptimized.totalDays).toBeGreaterThanOrEqual(4); + } + }); + + it('should handle non-standard weekend configurations', () => { + const holidays = [ + { date: new Date(TEST_YEAR, 0, 1), name: 'Holiday' }, + ]; + const optimizedDays = optimizeDaysOff(holidays, TEST_YEAR, 5, CUSTOM_WEEKENDS); + const periods = calculateConsecutiveDaysOff(holidays, optimizedDays, TEST_YEAR, CUSTOM_WEEKENDS); + + optimizedDays.forEach(date => { + expect(CUSTOM_WEEKENDS).not.toContain(date.getDay()); + }); + + expect(Array.isArray(periods)).toBe(true); + }); + }); +}); diff --git a/src/lib/holidayUtils.ts b/src/lib/holidayUtils.ts index 35296c7..51acb95 100644 --- a/src/lib/holidayUtils.ts +++ b/src/lib/holidayUtils.ts @@ -31,49 +31,56 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode? } // Find optimal placement of PTO days to maximize consecutive time off -export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6]): Date[] { +export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6], startDate?: Date): Date[] { + const effectiveStartDate = startDate || new Date(year, 0, 1); + const filteredHolidays = holidays.filter(h => h.date.getFullYear() === year && h.date >= effectiveStartDate); const allDaysOff = new Set([ - ...holidays.filter(h => h.date.getFullYear() === year).map(h => dateKey(h.date)), - ...getWeekends(year, weekendDays).map(d => dateKey(d)) + ...filteredHolidays.map(h => dateKey(h.date)), + ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d)) ]); - const gaps = findGaps(allDaysOff, year, weekendDays); + const gaps = findGaps(allDaysOff, year, weekendDays, effectiveStartDate); return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays); } // Calculate periods of consecutive days off (weekends + holidays + PTO) -export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6]) { +export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6], startDate?: Date) { + const effectiveStartDate = startDate || new Date(year, 0, 1); + const filteredHolidays = holidays.filter(h => h.date >= effectiveStartDate); + const filteredOptimizedDaysOff = optimizedDaysOff.filter(d => d >= effectiveStartDate); + const allDaysOff = new Set([ - ...holidays.map(h => dateKey(h.date)), - ...optimizedDaysOff.map(d => dateKey(d)), - ...getWeekends(year, weekendDays).map(d => dateKey(d)) + ...filteredHolidays.map(h => dateKey(h.date)), + ...filteredOptimizedDaysOff.map(d => dateKey(d)), + ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d)) ]); const consecutiveDaysOff = []; let currentGroup = []; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { - if (isWeekend(d, weekendDays) || isHoliday(d, holidays) || allDaysOff.has(dateKey(d))) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + if (isWeekend(d, weekendDays) || isHoliday(d, filteredHolidays) || allDaysOff.has(dateKey(d))) { currentGroup.push(new Date(d)); } else if (currentGroup.length > 0) { if (isValidConsecutiveGroup(currentGroup, weekendDays)) { - consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); + consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff)); } currentGroup = []; } } if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) { - consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); + consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff)); } return consecutiveDaysOff; } // Get all weekend days for a year -function getWeekends(year: number, weekendDays: number[]): Date[] { +function getWeekends(year: number, weekendDays: number[], startDate?: Date): Date[] { + const effectiveStartDate = startDate || new Date(year, 0, 1); const weekends = []; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) { weekends.push(new Date(d)); } @@ -82,11 +89,12 @@ function getWeekends(year: number, weekendDays: number[]): Date[] { } // Find gaps between days off that could be filled with PTO -function findGaps(allDaysOff: Set, year: number, weekendDays: number[]) { +function findGaps(allDaysOff: Set, year: number, weekendDays: number[], startDate?: Date) { + const effectiveStartDate = startDate || new Date(year, 0, 1); const gaps = []; let gapStart = null; - for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { + for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) { if (!gapStart) gapStart = new Date(d); } else if (gapStart) { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5fa057c..2f109d3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -17,6 +17,8 @@ let daysOff: number = 0; let optimizedDaysOff: Date[] = []; let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = []; + let showExcludedMonths: boolean = true; + let visibleMonths: number[] = []; let countriesInput: HTMLInputElement | null = null; let statesInput: HTMLInputElement | null = null; let showHowItWorks: boolean = false; @@ -35,12 +37,27 @@ let showWeekendSettings: boolean = false; let weekendDays: number[] = []; + // Start date state + let startDate: Date = new Date(new Date().getFullYear(), 0, 1); + let showDatePicker: boolean = false; + let datePickerValue: string = ''; + $: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; - $: if (selectedCountryCode || selectedStateCode || daysOff || year) { + $: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) { updateHolidays(); } + // Reactive: when year changes, load start date for that year + $: if (year !== undefined && year) { + startDate = getStartDate(year); + } + + // Reactive: when startDate or year changes, update excluded months visibility + $: if (year !== undefined && year && startDate) { + showExcludedMonths = !hasExcludedMonths(); + } + $: if (daysOff) { localStorage.setItem('daysOff', daysOff.toString()); } @@ -59,8 +76,11 @@ const stateName = target.value; selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || ''; selectedState = stateName; - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); + // Save state per country + if (selectedCountryCode) { + localStorage.setItem(`selectedState_${selectedCountryCode}`, selectedState); + localStorage.setItem(`selectedStateCode_${selectedCountryCode}`, selectedStateCode); + } } onMount(() => { @@ -73,14 +93,22 @@ const storedYear = localStorage.getItem('year'); const storedCountry = localStorage.getItem('selectedCountry'); const storedDaysOff = localStorage.getItem('daysOff'); - const storedState = localStorage.getItem('selectedState'); - const storedStateCode = localStorage.getItem('selectedStateCode'); year = storedYear ? parseInt(storedYear, 10) : defaultYear; selectedCountry = storedCountry || defaultCountry; daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff; - selectedState = storedState || ''; - selectedStateCode = storedStateCode || ''; + + // Load state per country + const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; + if (countryCode) { + selectedState = localStorage.getItem(`selectedState_${countryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${countryCode}`) || ''; + } else { + selectedState = ''; + selectedStateCode = ''; + } + startDate = getStartDate(year); + // showExcludedMonths will be set by reactive statement updateHolidays(); }); @@ -113,14 +141,17 @@ function handleCountryChange(event: Event) { const target = event.target as HTMLInputElement; const fullValue = target.value; - if (selectedCountryCode) { - daysOff = ptoData[selectedCountryCode] || 0; - selectedState = ''; // Reset state - selectedStateCode = ''; // Reset state code - updateStatesList(selectedCountryCode); // Update states list for the new country + selectedCountry = fullValue; + // Get the country code for the new country (selectedCountryCode will update reactively) + const newCountryCode = Object.keys(countriesList).find(code => countriesList[code] === fullValue) || ''; + if (newCountryCode) { + // Update days off to the new country's default + daysOff = ptoData[newCountryCode] || 0; + // Load state for the new country + selectedState = localStorage.getItem(`selectedState_${newCountryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${newCountryCode}`) || ''; + // updateStatesList and updateHolidays will be called by reactive statements localStorage.setItem('selectedCountry', selectedCountry); - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); localStorage.setItem('daysOff', daysOff.toString()); } } @@ -135,8 +166,8 @@ hidden: isHolidayHidden(holiday) })); const visibleHolidays = holidays.filter(h => !h.hidden); - optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays); - consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays); + optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate); + consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate); } else { holidays = []; optimizedDaysOff = []; @@ -147,14 +178,18 @@ function resetToDefault() { year = defaultYear; selectedCountry = defaultCountry; - selectedState = ''; - selectedStateCode = ''; - daysOff = defaultDaysOff; + const defaultCountryCode = Object.keys(countriesList).find(code => countriesList[code] === defaultCountry) || ''; + // Load state for default country + if (defaultCountryCode) { + selectedState = localStorage.getItem(`selectedState_${defaultCountryCode}`) || ''; + selectedStateCode = localStorage.getItem(`selectedStateCode_${defaultCountryCode}`) || ''; + } else { + selectedState = ''; + selectedStateCode = ''; + } + // Keep current daysOff value, don't reset it localStorage.setItem('year', year.toString()); localStorage.setItem('selectedCountry', selectedCountry); - localStorage.setItem('selectedState', selectedState); - localStorage.setItem('selectedStateCode', selectedStateCode); - localStorage.setItem('daysOff', daysOff.toString()); } function handleKeyDown(event: KeyboardEvent) { @@ -252,6 +287,156 @@ $: visibleHolidaysCount = holidays.filter(h => !h.hidden).length; + // Get start date for a given year from localStorage + function getStartDate(year: number): Date { + try { + const stored = localStorage.getItem('startDates'); + if (stored) { + const startDates: string[] = JSON.parse(stored); + // Find date that matches the year (extract year from date string) + const dateStr = startDates.find(date => { + const dateYear = parseInt(date.split('-')[0] || date.split('T')[0].split('-')[0]); + return dateYear === year; + }); + if (dateStr) { + // Parse date string - handle both YYYY-MM-DD and ISO format + const parsed = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr; + const [y, m, d] = parsed.split('-').map(Number); + return new Date(y, m - 1, d); + } + } + } catch (e) { + console.error('Error loading start date:', e); + } + return new Date(year, 0, 1); // Default to Jan 1st + } + + // Save start date for a given year to localStorage + function saveStartDate(year: number, date: Date) { + try { + const stored = localStorage.getItem('startDates'); + let startDates: string[] = stored ? JSON.parse(stored) : []; + // Remove existing date for this year (extract year from date string) + startDates = startDates.filter(dateStr => { + const dateYear = parseInt(dateStr.split('-')[0] || dateStr.split('T')[0].split('-')[0]); + return dateYear !== year; + }); + // Add new date + startDates.push(formatDateForInput(date)); + localStorage.setItem('startDates', JSON.stringify(startDates)); + } catch (e) { + console.error('Error saving start date:', e); + } + } + + // Format date as YYYY-MM-DD for date input (no timezone conversion) + function formatDateForInput(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + // Format start date for display + function formatStartDate(date: Date): string { + const today = new Date(); + if (date.getTime() === new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime() && date.getFullYear() === year) { + return 'Today'; + } + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const day = date.getDate(); + const suffix = getDaySuffix(day); + return `${monthNames[date.getMonth()]} ${day}${suffix}`; + } + + // Get day suffix (st, nd, rd, th) + function getDaySuffix(day: number): string { + if (day > 3 && day < 21) return 'th'; + switch (day % 10) { + case 1: return 'st'; + case 2: return 'nd'; + case 3: return 'rd'; + default: return 'th'; + } + } + + // Handle start date change + function handleStartDateChange(newDate: Date) { + startDate = newDate; + saveStartDate(year, newDate); + updateHolidays(); + // showExcludedMonths will be updated by reactive statement + } + + // Handle date picker input change (auto-save) + function handleDatePickerChange() { + if (datePickerValue) { + // Parse YYYY-MM-DD format in local time (consistent with getStartDate) + const [y, m, d] = datePickerValue.split('-').map(Number); + const newDate = new Date(y, m - 1, d); + handleStartDateChange(newDate); + } + } + + // Set start date to today + function setStartDateToToday() { + const today = new Date(); + if (today.getFullYear() === year) { + handleStartDateChange(new Date(today.getFullYear(), today.getMonth(), today.getDate())); + showDatePicker = false; + } + } + + // Reset start date to Jan 1st + function resetStartDateToJan1() { + const jan1st = new Date(year, 0, 1); + handleStartDateChange(jan1st); + showDatePicker = false; + } + + // Check if today is in the current year + function isTodayInYear(): boolean { + const today = new Date(); + return today.getFullYear() === year; + } + + // Helper: Get start date normalized to the current year + function getStartDateInYear(): Date { + return new Date(year, startDate.getMonth(), startDate.getDate()); + } + + // Check if a month is active (not entirely before the start date) + function isMonthActive(monthIndex: number): boolean { + const startDateInYear = getStartDateInYear(); + const startMonth = startDateInYear.getMonth(); + + // Month is active if: + // 1. The start date falls within this month (same month), OR + // 2. The month starts on or after the start date (later month) + // This means only months entirely before the start date's month are excluded + return monthIndex >= startMonth; + } + + // Check if start date is Jan 1st + function isStartDateJan1st(): boolean { + const startDateInYear = getStartDateInYear(); + return startDateInYear.getTime() === new Date(year, 0, 1).getTime(); + } + + // Check if there are any excluded months (months entirely before the start date) + function hasExcludedMonths(): boolean { + return months.some(month => !isMonthActive(month)); + } + + // Filter months based on showExcludedMonths setting + // Explicitly depend on startDate, year, and showExcludedMonths to ensure proper reactivity + // Use a computed value that depends on all relevant variables + $: visibleMonths = (startDate && year !== undefined && year) + ? (showExcludedMonths + ? months + : months.filter(month => isMonthActive(month))) + : []; + function toggleWeekendDay(dayNumber: number) { if (weekendDays.includes(dayNumber)) { weekendDays = weekendDays.filter(d => d !== dayNumber); @@ -318,7 +503,7 @@ .content-box p { text-align: center; - line-height: 2; + line-height: 3; } input { @@ -365,6 +550,29 @@ } } + .toggle-excluded-months-container { + text-align: center; + margin: 20px 0; + } + + .toggle-excluded-months { + padding: 8px 16px; + background-color: #333; + border: 1px solid #555; + border-radius: 5px; + color: #fff; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.3s; + width: auto; + display: inline-block; + white-space: nowrap; + } + + .toggle-excluded-months:hover { + background-color: #444; + } + .calendar-container { width: 100%; max-width: 300px; @@ -557,7 +765,6 @@ } .content-box button { - margin-left: 10px; background-color: #444; border: none; color: #fff; @@ -641,6 +848,161 @@ margin-bottom: 15px; color: #fff; } + + .start-date-link { + text-decoration: underline; + text-decoration-style: dotted; + cursor: pointer; + color: inherit; + } + + .start-date-link:hover { + text-decoration-style: solid; + } + + .date-picker-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .date-picker-modal { + background-color: #222; + border-radius: 10px; + padding: 25px; + max-width: min(400px, calc(100vw - 40px)); + width: 100%; + box-sizing: border-box; + position: relative; + color: #fff; + } + + .date-picker-close { + position: absolute; + top: 10px; + right: 10px; + background: transparent; + border: none; + color: #fff; + font-size: 24px; + cursor: pointer; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + line-height: 1; + } + + .date-picker-close:hover, + .date-picker-close:active, + .date-picker-close:focus { + background: transparent; + color: #ccc; + } + + .date-picker-modal h3 { + margin: 0 0 10px 0; + padding: 0; + text-align: center; + font-size: 1.5em; + } + + .date-picker-modal p { + margin: 0 0 20px 0; + padding: 0; + text-align: center; + color: #ccc; + } + + .date-picker-controls { + margin: 0 0 20px 0; + padding: 0; + width: 100%; + } + + .date-input { + width: 100%; + padding: 10px; + font-size: 1em; + background-color: #222; + border: 1px solid #555; + border-radius: 5px; + color: #fff; + box-sizing: border-box; + text-align: center; + margin: 0; + display: block; + } + + .date-input:focus { + outline: 2px solid #61dafb; + border-color: #61dafb; + } + + /* Make calendar icon white */ + .date-input::-webkit-calendar-picker-indicator { + filter: invert(1); + cursor: pointer; + } + + .date-input::-webkit-inner-spin-button, + .date-input::-webkit-clear-button { + filter: invert(1); + } + + .date-picker-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin: 0 0 15px 0; + width: 100%; + box-sizing: border-box; + } + + .date-picker-button, + .date-picker-save { + margin: 0; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + box-sizing: border-box; + } + + .date-picker-button { + flex: 1; + min-width: 0; + padding: 20px; + background-color: #444; + color: #fff; + font-size: 0.9em; + } + + .date-picker-button:hover { + background-color: #555; + } + + .date-picker-save { + width: 100%; + margin: 0; + font-size: 1.1em; + font-weight: bold; + background-color: #61dafb; + color: #000; + } + + .date-picker-save:hover { + background-color: #4fa8c5; + }
@@ -689,15 +1051,27 @@ {daysOff} - days off in + days off from + { showDatePicker = true; datePickerValue = formatDateForInput(startDate); }} class="bold start-date-link"> + {@html formatStartDate(startDate)} + + until the end of {year}

- {#if year !== defaultYear || selectedCountry !== defaultCountry || daysOff !== defaultDaysOff} - Reset to my country + {#if year !== defaultYear || selectedCountry !== defaultCountry} + {@const yearDifferent = year !== defaultYear} + {@const countryDifferent = selectedCountry !== defaultCountry} + + {yearDifferent && countryDifferent + ? 'Reset to current country and year' + : yearDifferent + ? 'Reset to current year' + : 'Reset to current country'} + {/if} @@ -781,13 +1155,27 @@
{/if} + {#if hasExcludedMonths()} +
+ +
+ {/if} +
- {#each months as month} + {#each visibleMonths as month}
+ {#if showDatePicker} +
showDatePicker = false}> +
+ +

Set Start Date

+

Choose when your time off period begins for {year}

+
+ +
+
+ {#if isTodayInYear()} + + {/if} + {#if !isStartDateJan1st()} + + {/if} +
+ +
+
+ {/if} + diff --git a/vite.config.ts b/vite.config.ts index bbf8c7d..b233f82 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,5 +2,10 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], + test: { + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'] + } });