diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 57a469ad1fa..27756c6ff34 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -2744,7 +2744,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3464,7 +3464,7 @@ importers: version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) ../../foundations/core/packages/measurements-otlp: dependencies: @@ -3549,7 +3549,7 @@ importers: version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) ../../foundations/core/packages/model: dependencies: @@ -3720,7 +3720,7 @@ importers: version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) ../../foundations/core/packages/query: dependencies: @@ -4046,7 +4046,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5700,7 +5700,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5788,7 +5788,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5858,7 +5858,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -5922,7 +5922,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6356,7 +6356,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6444,7 +6444,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6514,7 +6514,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6608,7 +6608,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6714,7 +6714,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6823,7 +6823,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -6944,7 +6944,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7035,7 +7035,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7138,7 +7138,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7226,7 +7226,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7350,7 +7350,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7498,7 +7498,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7559,7 +7559,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7623,7 +7623,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7696,7 +7696,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7760,7 +7760,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7884,7 +7884,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -7978,7 +7978,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8060,7 +8060,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8148,7 +8148,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8248,7 +8248,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8321,7 +8321,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8415,7 +8415,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8485,7 +8485,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8567,7 +8567,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8661,7 +8661,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8776,7 +8776,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8891,7 +8891,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -8976,7 +8976,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9049,7 +9049,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9149,7 +9149,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9213,7 +9213,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9289,7 +9289,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9353,7 +9353,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9432,7 +9432,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9535,7 +9535,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9723,7 +9723,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9805,7 +9805,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -9884,7 +9884,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10017,7 +10017,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10102,7 +10102,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10181,7 +10181,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10281,7 +10281,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10345,7 +10345,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10415,7 +10415,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10488,7 +10488,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10561,7 +10561,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10622,7 +10622,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10695,7 +10695,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10777,7 +10777,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10838,7 +10838,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10914,7 +10914,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -10978,7 +10978,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11051,7 +11051,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11115,7 +11115,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11185,7 +11185,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11252,7 +11252,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11328,7 +11328,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11404,7 +11404,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11483,7 +11483,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11556,7 +11556,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11623,7 +11623,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11702,7 +11702,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11775,7 +11775,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11848,7 +11848,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11912,7 +11912,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -11979,7 +11979,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12058,7 +12058,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12125,7 +12125,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12195,7 +12195,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12274,7 +12274,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12347,7 +12347,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12411,7 +12411,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12505,7 +12505,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12575,7 +12575,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12657,7 +12657,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12733,7 +12733,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12845,7 +12845,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -12945,7 +12945,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13033,7 +13033,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13160,7 +13160,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13233,7 +13233,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13354,7 +13354,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13481,7 +13481,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13593,7 +13593,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13666,7 +13666,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13745,7 +13745,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13827,7 +13827,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13900,7 +13900,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -13958,7 +13958,52 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + ../../packages/gantt: + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-svelte: + specifier: ^2.35.1 + version: 2.46.1(eslint@8.57.1)(svelte@4.2.20)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14180,7 +14225,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14307,7 +14352,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14380,7 +14425,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14529,7 +14574,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14678,7 +14723,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -14887,7 +14932,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15115,7 +15160,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15167,7 +15212,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15319,7 +15364,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15371,7 +15416,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15544,7 +15589,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15596,7 +15641,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15778,7 +15823,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15933,7 +15978,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -15988,7 +16033,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16137,7 +16182,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16189,7 +16234,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16368,7 +16413,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16420,7 +16465,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16623,7 +16668,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16675,7 +16720,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16878,7 +16923,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -16930,7 +16975,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17136,7 +17181,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17188,7 +17233,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17382,7 +17427,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17434,7 +17479,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17679,7 +17724,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17731,7 +17776,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17937,7 +17982,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -17989,7 +18034,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18204,7 +18249,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18256,7 +18301,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18489,7 +18534,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18541,7 +18586,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18783,7 +18828,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -18838,7 +18883,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19080,7 +19125,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19232,7 +19277,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19284,7 +19329,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19439,7 +19484,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19491,7 +19536,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19631,7 +19676,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19792,7 +19837,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -19844,7 +19889,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20008,7 +20053,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20060,7 +20105,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20266,7 +20311,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20318,7 +20363,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20482,7 +20527,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20534,7 +20579,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20686,7 +20731,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20741,7 +20786,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20893,7 +20938,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -20945,7 +20990,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21103,7 +21148,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21155,7 +21200,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21349,7 +21394,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21401,7 +21446,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21571,7 +21616,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21623,7 +21668,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21805,7 +21850,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -21857,7 +21902,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22021,7 +22066,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22146,7 +22191,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22198,7 +22243,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22389,7 +22434,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22441,7 +22486,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22596,7 +22641,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22648,7 +22693,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22842,7 +22887,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -22894,7 +22939,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23076,7 +23121,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23131,7 +23176,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23349,7 +23394,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23401,7 +23446,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23459,7 +23504,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typed-emitter: specifier: ^2.1.0 version: 2.1.0 @@ -23514,7 +23559,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23687,7 +23732,7 @@ importers: version: 1.93.3 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23739,7 +23784,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23918,7 +23963,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -23970,7 +24015,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24134,7 +24179,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24186,7 +24231,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24238,7 +24283,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24293,7 +24338,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24451,7 +24496,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24506,7 +24551,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24664,7 +24709,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24719,7 +24764,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24919,7 +24964,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24974,7 +25019,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25156,7 +25201,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25208,7 +25253,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25363,7 +25408,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25415,7 +25460,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25576,7 +25621,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25628,7 +25673,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25822,7 +25867,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -25874,7 +25919,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26095,7 +26140,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26147,7 +26192,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26320,7 +26365,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26372,7 +26417,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26572,7 +26617,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26621,7 +26666,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26673,7 +26718,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26810,7 +26855,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -26862,7 +26907,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27014,7 +27059,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27066,7 +27111,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27221,7 +27266,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27273,7 +27318,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27488,7 +27533,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27540,7 +27585,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27737,7 +27782,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27789,7 +27834,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -27962,7 +28007,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28014,7 +28059,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28250,7 +28295,7 @@ importers: version: 1.93.3 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28302,7 +28347,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28640,7 +28685,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28692,7 +28737,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28865,6 +28910,9 @@ importers: '@hcengineering/core': specifier: workspace:^0.7.26 version: link:../../foundations/core/packages/core + '@hcengineering/notification': + specifier: workspace:^0.7.0 + version: link:../notification '@hcengineering/platform': specifier: workspace:^0.7.20 version: link:../../foundations/core/packages/platform @@ -28925,7 +28973,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -28977,7 +29025,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29026,6 +29074,9 @@ importers: '@hcengineering/core': specifier: workspace:^0.7.26 version: link:../../foundations/core/packages/core + '@hcengineering/gantt': + specifier: workspace:^0.7.0 + version: link:../../packages/gantt '@hcengineering/kanban': specifier: workspace:^0.7.0 version: link:../../packages/kanban @@ -29092,9 +29143,15 @@ importers: '@hcengineering/workbench-resources': specifier: workspace:^0.7.0 version: link:../workbench-resources + '@tanstack/svelte-virtual': + specifier: ^3.13.6 + version: 3.13.34(svelte@4.2.20) fast-equals: specifier: ^5.2.2 version: 5.3.2 + jspdf: + specifier: ^2.5.2 + version: 2.5.2 svelte: specifier: ^4.2.20 version: 4.2.20 @@ -29216,7 +29273,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29268,7 +29325,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29456,7 +29513,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29508,7 +29565,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29663,7 +29720,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29718,7 +29775,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29927,7 +29984,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -29979,7 +30036,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -30345,7 +30402,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31702,7 +31759,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31790,7 +31847,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31848,7 +31905,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31936,7 +31993,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -31994,7 +32051,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32061,7 +32118,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32116,7 +32173,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32171,7 +32228,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32229,7 +32286,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32302,7 +32359,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32360,7 +32417,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32442,7 +32499,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32500,7 +32557,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32591,7 +32648,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32649,7 +32706,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32701,7 +32758,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32765,7 +32822,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32844,7 +32901,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32902,7 +32959,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32993,7 +33050,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33054,7 +33111,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33121,7 +33178,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33176,7 +33233,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33231,7 +33288,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33289,7 +33346,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33365,7 +33422,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33420,7 +33477,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33481,7 +33538,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33539,7 +33596,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33615,7 +33672,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33667,7 +33724,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33734,7 +33791,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33789,7 +33846,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33859,7 +33916,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33920,7 +33977,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -33996,7 +34053,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34060,7 +34117,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34145,7 +34202,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34203,7 +34260,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34270,7 +34327,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34343,7 +34400,7 @@ importers: version: 3.4.0(prettier@3.6.2)(svelte@4.2.20) ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34401,7 +34458,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34459,7 +34516,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34529,7 +34586,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34584,7 +34641,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34663,7 +34720,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34724,7 +34781,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34785,7 +34842,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34840,7 +34897,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34895,7 +34952,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -34953,7 +35010,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35029,7 +35086,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35096,7 +35153,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35184,7 +35241,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35242,7 +35299,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35303,7 +35360,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35388,7 +35445,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35446,7 +35503,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35534,7 +35591,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35592,7 +35649,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35662,7 +35719,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35720,7 +35777,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35778,7 +35835,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -35869,7 +35926,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -36078,7 +36135,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -36169,7 +36226,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -36562,7 +36619,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -37188,7 +37245,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -38503,7 +38560,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -38555,7 +38612,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -38857,7 +38914,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -39348,7 +39405,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -39427,7 +39484,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -39497,7 +39554,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -41525,7 +41582,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) ts-node-dev: specifier: ^2.0.0 version: 2.0.0(@types/node@22.19.0)(typescript@5.9.3) @@ -41922,7 +41979,7 @@ importers: version: 3.6.2 ts-jest: specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -44474,6 +44531,14 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tanstack/svelte-virtual@3.13.34': + resolution: {integrity: sha512-vmHha/+DzPiNaaKsz6CNsClqXLNtBPjeRt3I5+w8Y/7sIp7dwp/UNVGycju2xoQVsohgjg+oDEOc7kLSG+XtXQ==} + peerDependencies: + svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/virtual-core@3.17.6': + resolution: {integrity: sha512-h0/Ebo18CkOrChlQIhNtQkM5ySUnh/GumQ/D1st3hG2HWUPEF+ILUc2k29UtivCi/9G7w7G3/f7Xyd5cCFbKBw==} + '@telegraf/entity@0.5.0': resolution: {integrity: sha512-4oHOoXcrNaK44zPq4GuTgMmUvCSQxJRVAuPFzVtSeiKzCBJnLeYblsMqWotokhrZSDnNpunC1sxhqI3iVYa/sg==} @@ -45176,6 +45241,9 @@ packages: '@types/querystringify@2.0.2': resolution: {integrity: sha512-7d6OQK6pJ//zE32XLK3vI6GHYhBDcYooaRco9cKFGNu59GVatL5+u7rkiAViq44DxDTd/7QQNBWSDHfJGBz/Pw==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -45721,6 +45789,11 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + atomically@1.7.0: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} @@ -45817,6 +45890,10 @@ packages: bare-url@2.3.2: resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -45939,6 +46016,11 @@ packages: btoa-lite@1.0.0: resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + buffer-alloc-unsafe@1.1.0: resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} @@ -46069,6 +46151,10 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + centra@2.7.0: resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} @@ -46472,6 +46558,9 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-loader@5.2.7: resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} engines: {node: '>= 10.13.0'} @@ -46981,6 +47070,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@2.5.9: + resolution: {integrity: sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==} + dompurify@3.3.0: resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} @@ -47614,6 +47706,9 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + file-api@0.10.4: resolution: {integrity: sha512-RVBXJGmsnQxokdpy264pmsdBjbUuxE6QT2xxhOrO2pzwTetbTNoWVFgkONFWmopm5mellsXrQIQhMY9fjufi9g==} @@ -47896,7 +47991,7 @@ packages: glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} @@ -48107,6 +48202,10 @@ packages: webpack: optional: true + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} @@ -48848,6 +48947,9 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jspdf@2.5.2: + resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} + jszip@2.7.0: resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==} @@ -50043,6 +50145,9 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -50469,6 +50574,9 @@ packages: resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} engines: {node: '>=12'} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + random-bytes@1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -50641,6 +50749,10 @@ packages: rfc6902@5.1.2: resolution: {integrity: sha512-zxcb+PWlE8PwX0tiKE6zP97THQ8/lHmeiwucRrJ3YFupWEmp25RmFSlB1dNTqjkovwqG4iq+u1gzJMBS3um8mA==} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -51043,6 +51155,10 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} @@ -51308,6 +51424,10 @@ packages: resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} engines: {node: '>=16'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + svgo-loader@3.0.3: resolution: {integrity: sha512-6YdWYge3h0aCb8xHvPhGP4hofIU1OWfZm0I8bteab7hddeRN4fl3aIkN8Z/ZB/ji9QrMOd6C8wT8F1p31GUwuQ==} @@ -51384,6 +51504,9 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -51788,6 +51911,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -55571,6 +55697,13 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tanstack/svelte-virtual@3.13.34(svelte@4.2.20)': + dependencies: + '@tanstack/virtual-core': 3.17.6 + svelte: 4.2.20 + + '@tanstack/virtual-core@3.17.6': {} + '@telegraf/entity@0.5.0': dependencies: '@telegraf/types': 7.1.0 @@ -56383,6 +56516,9 @@ snapshots: '@types/querystringify@2.0.2': {} + '@types/raf@3.4.3': + optional: true + '@types/range-parser@1.2.7': {} '@types/readdir-glob@1.1.5': @@ -57036,6 +57172,8 @@ snapshots: at-least-node@1.0.0: {} + atob@2.1.2: {} + atomically@1.7.0: {} autolinker@4.0.0: @@ -57154,6 +57292,9 @@ snapshots: bare-path: 3.0.0 optional: true + base64-arraybuffer@1.0.2: + optional: true + base64-js@1.5.1: {} base64url@3.0.1: {} @@ -57272,6 +57413,8 @@ snapshots: btoa-lite@1.0.0: {} + btoa@1.2.1: {} + buffer-alloc-unsafe@1.1.0: {} buffer-alloc@1.2.0: @@ -57440,6 +57583,18 @@ snapshots: caniuse-lite@1.0.30001793: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.46.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + centra@2.7.0: dependencies: follow-redirects: 1.15.11 @@ -57896,6 +58051,11 @@ snapshots: crypto-js@4.2.0: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + css-loader@5.2.7(webpack@5.102.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -58434,6 +58594,9 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@2.5.9: + optional: true + dompurify@3.3.0: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -59264,6 +59427,8 @@ snapshots: fecha@4.2.3: {} + fflate@0.8.3: {} + file-api@0.10.4: dependencies: File: 0.10.2 @@ -59882,6 +60047,12 @@ snapshots: optionalDependencies: webpack: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4) + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -60902,6 +61073,18 @@ snapshots: ms: 2.1.3 semver: 7.7.3 + jspdf@2.5.2: + dependencies: + '@babel/runtime': 7.28.4 + atob: 2.1.2 + btoa: 1.2.1 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.46.0 + dompurify: 2.5.9 + html2canvas: 1.4.1 + jszip@2.7.0: dependencies: pako: 1.0.11 @@ -61713,7 +61896,7 @@ snapshots: node-loader@2.0.0(webpack@5.102.1): dependencies: loader-utils: 2.0.4 - webpack: 5.102.1 + webpack: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4) node-localstorage@2.2.1: dependencies: @@ -62145,6 +62328,9 @@ snapshots: pend@1.2.0: {} + performance-now@2.1.0: + optional: true + periscopic@3.1.0: dependencies: '@types/estree': 1.0.8 @@ -62631,6 +62817,11 @@ snapshots: quick-lru@6.1.2: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + random-bytes@1.0.0: {} randombytes@2.1.0: @@ -62810,6 +63001,9 @@ snapshots: rfc6902@5.1.2: {} + rgbcolor@1.0.1: + optional: true + rimraf@2.7.1: dependencies: glob: 7.2.3 @@ -63309,6 +63503,9 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackblur-canvas@2.7.0: + optional: true + standardwebhooks@1.0.0: dependencies: '@stablelib/base64': 1.0.1 @@ -63603,6 +63800,9 @@ snapshots: magic-string: 0.30.21 periscopic: 3.1.0 + svg-pathdata@6.0.3: + optional: true + svgo-loader@3.0.3: dependencies: loader-utils: 2.0.4 @@ -63778,6 +63978,11 @@ snapshots: text-hex@1.0.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + text-table@0.2.0: {} thenify-all@1.6.0: @@ -63896,47 +64101,6 @@ snapshots: esbuild: 0.25.12 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 29.7.0 - '@jest/types': 30.2.0 - babel-jest: 29.7.0(@babel/core@7.28.5) - esbuild: 0.25.12 - jest-util: 30.2.0 - - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 29.7.0 - '@jest/types': 30.2.0 - babel-jest: 29.7.0(@babel/core@7.28.5) - jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.102.1): dependencies: chalk: 4.1.2 @@ -64215,6 +64379,11 @@ snapshots: utils-merge@1.0.1: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + uuid@11.1.0: {} uuid@3.4.0: {} diff --git a/foundations/server/packages/elastic/src/adapter.ts b/foundations/server/packages/elastic/src/adapter.ts index 1da9e3711e6..6cefedce0eb 100644 --- a/foundations/server/packages/elastic/src/adapter.ts +++ b/foundations/server/packages/elastic/src/adapter.ts @@ -314,17 +314,71 @@ class ElasticAdapter implements FullTextAdapter { from: number | undefined ): Promise { if (query.$search === undefined) return [] + const raw = String(query.$search) + // Route field-targeted queries (e.g. `searchTitle:value`, `identifier:HULY-`, + // `comments.message:foo`) through `query_string` so ES can parse the + // field-targeted clauses and apply per-field boosts. Restrict the + // detector to the set of fields we actually index — typing a bare colon + // such as `POC: design review` or a URL must NOT silently route to + // `query_string` (which would throw a parsing exception and surface as + // zero hits). Anything else falls back to `simple_query_string` for full + // backwards compatibility. + // KEEP IN SYNC with packages/ui SearchInputAdvanced.encoder.ts + // ES_NATIVE_FIELDS (and vice versa). The client only emits a `field:value` + // clause for a field it believes the server recognises; if the two lists + // drift, a client-routed clause hits a field the adapter does not treat as + // query_string and silently fails to parse. + // + // SECURITY: `query_string` lets a raw `field:value` clause target + // ANY indexed field (e.g. `space:`, `modifiedBy:`, `attachedTo:`), + // not just the `fields` whitelist below (that list only picks the DEFAULT + // fields for bare terms). This is intentionally NOT gated here because the + // hits this method returns are never authoritative results — they are only a + // candidate id-set. The two enclosing guards make field-targeting safe: + // 1. `workspaceId` is a hard `bool.must` term (see the request below), so + // no clause can reach another workspace's documents. + // 2. Within the workspace, every consumer re-filters by space ACL. The + // only caller is pods/fulltext `/api/v1/search` → FullTextMiddleware, + // which sits BELOW SpaceSecurityMiddleware in the pipeline. That + // middleware runs `provideFindAll({ _id: { $in: }, ...q })` + // where `q` still carries the space constraint SpaceSecurityMiddleware + // injected upstream (or, in its >85%-allowed fast path, post-filters + // the result via `clientFilterSpaces`). A forbidden-space doc surfaced + // by a crafted `space:` clause is therefore dropped at the DB findAll / + // result filter and never reaches the client — no content or existence + // oracle. Checked against the server-pipeline middleware order. + // + const KNOWN_FIELD_RE = + /(^|\s)(searchTitle|searchShortTitle|identifier|description\.plain|comments\.message|fulltextSummary)\s*:/i + const usesQueryString = KNOWN_FIELD_RE.test(raw) + const queryBlock: any = usesQueryString + ? { + query_string: { + query: raw, + fields: [ + 'searchTitle^3', + 'searchShortTitle^2', + 'identifier^2', + 'description.plain', + 'comments.message^0.7', + 'fulltextSummary' + ], + default_operator: 'AND', + allow_leading_wildcard: false + } + } + : { + simple_query_string: { + query: raw, + analyze_wildcard: true, + flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE', + default_operator: 'and' + } + } const request: any = { bool: { must: [ - { - simple_query_string: { - query: query.$search, - analyze_wildcard: true, - flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE', - default_operator: 'and' - } - }, + queryBlock, { term: { workspaceId diff --git a/models/server-tracker/src/index.ts b/models/server-tracker/src/index.ts index 579918fa8e9..0c508d42436 100644 --- a/models/server-tracker/src/index.ts +++ b/models/server-tracker/src/index.ts @@ -73,6 +73,14 @@ export function createModel (builder: Builder): void { } }) + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverTracker.trigger.OnDependencyShiftRequest, + txMatch: { + _class: core.class.TxCreateDoc, + objectClass: tracker.class.DependencyShiftRequest + } + }) + builder.mixin( tracker.ids.AssigneeNotification, notification.class.NotificationType, diff --git a/models/tracker/src/__tests__/migration.test.ts b/models/tracker/src/__tests__/migration.test.ts index 68f452b2415..f54086addcc 100644 --- a/models/tracker/src/__tests__/migration.test.ts +++ b/models/tracker/src/__tests__/migration.test.ts @@ -5,11 +5,12 @@ // you may not use this file except in compliance with the License. // +import { DOMAIN_TX } from '@hcengineering/core' import { DOMAIN_TASK } from '@hcengineering/model-task' import tracker from '@hcengineering/tracker' import { DOMAIN_TRACKER } from '../types' -import { migrateAddStartDate } from '../migration' +import { migrateAddStartDate, migrateRelationActivityAttachment } from '../migration' describe('migrateAddStartDate', () => { it('sets startDate=null on every Issue lacking the field (DOMAIN_TASK)', async () => { @@ -47,3 +48,24 @@ describe('migrateAddStartDate', () => { expect(update).toHaveBeenCalledTimes(2) }) }) + +describe('migrateRelationActivityAttachment', () => { + it('looks up the relation create-tx in DOMAIN_TX (not DOMAIN_MODEL_TX)', async () => { + const findDomains: string[] = [] + const client: any = { + find: jest.fn(async (domain: string, q: any) => { + findDomains.push(domain) + // one broken remove-DUM on the first (DOMAIN_ACTIVITY) call: + if (domain === 'activity') { + return [{ _id: 'dum1', objectId: 'rel1', attachedToClass: 'x', updateCollection: undefined }] + } + return [] // tx lookup returns empty regardless of domain + }), + update: jest.fn(async () => undefined) + } + await migrateRelationActivityAttachment(client) + // The create-tx lookup MUST hit DOMAIN_TX, never DOMAIN_MODEL_TX. + expect(findDomains).toContain(DOMAIN_TX) + expect(findDomains).not.toContain('model_tx') + }) +}) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index baabea1381a..e785d304098 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -38,6 +38,8 @@ import { DOMAIN_TRACKER, TClassicProjectTypeData, TComponent, + TDependencyShiftedNotification, + TDependencyShiftRequest, TIssue, TIssueRelation, TIssueStatus, @@ -161,6 +163,34 @@ function defineNotifications (builder: Builder): void { tracker.ids.AssigneeNotification ) + // Notification on Dependency-Shift. + // The cascade bundle is created client-side as a CommonInboxNotification + // subclass (see `dependency-shift-send.ts`); this NotificationType wires it + // into the user's tracker notification group so settings/Inbox provider + // routing work the same as the assignee type. `field` is bound to the + // `cascadeToken` attribute so the auto-`dueDate` generated type stays + // distinct (and they don't compete on the same `dueDate` notify channel). + builder.createDoc( + notification.class.NotificationType, + core.space.Model, + { + hidden: false, + generated: false, + label: tracker.string.DependencyShifted, + group: tracker.ids.TrackerNotificationGroup, + field: 'cascadeToken', + txClasses: [core.class.TxCreateDoc], + objectClass: tracker.class.DependencyShiftedNotification, + templates: { + textTemplate: '{sender} shifted {trigger} — {count} dependent issues moved', + htmlTemplate: '

{sender} shifted {trigger} — {count} dependent issues moved

', + subjectTemplate: 'Dependency shift' + }, + defaultEnabled: true + }, + tracker.ids.DependencyShiftedNotification + ) + generateClassNotificationTypes( builder, tracker.class.Issue, @@ -196,7 +226,9 @@ function defineFilters (builder: Builder): void { key: 'milestone', component: view.component.ObjectFilter, showNested: false - } + }, + 'startDate', + 'dueDate' ], ignoreKeys: ['number', 'estimation', 'attachedTo'], getVisibleFilters: tracker.function.GetVisibleFilters @@ -453,7 +485,9 @@ export function createModel (builder: Builder): void { TRelatedIssueTarget, TTypeEstimation, TTypeRemainingTime, - TProjectTargetPreference + TProjectTargetPreference, + TDependencyShiftedNotification, + TDependencyShiftRequest ) builder.mixin(tracker.class.Project, core.class.Class, activity.mixin.ActivityDoc, {}) @@ -596,6 +630,35 @@ export function createModel (builder: Builder): void { tracker.ids.IssueRemovedActivityViewlet ) + // Activity-Log Remove-Detail Fix. + // Three symmetric viewlets so IssueRelation add/remove/update show up in + // the issue activity feed with a kind+lag+target.title snapshot instead + // of the previous empty "removed related to:" row. The + // RelationActivityPresenter reuses IssueRelationPresenter, which is + // already registered as the ObjectPresenter for tracker.class.IssueRelation + // (models/tracker/src/presenters.ts). + builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, { + objectClass: tracker.class.IssueRelation, + action: 'create', + icon: tracker.icon.Issue, + label: tracker.string.AddedRelation, + component: tracker.component.RelationActivityPresenter + }) + builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, { + objectClass: tracker.class.IssueRelation, + action: 'remove', + icon: tracker.icon.Issue, + label: tracker.string.RemovedRelation, + component: tracker.component.RelationActivityPresenter + }) + builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, { + objectClass: tracker.class.IssueRelation, + action: 'update', + icon: tracker.icon.Issue, + label: tracker.string.UpdatedRelation, + component: tracker.component.RelationActivityPresenter + }) + builder.createDoc( activity.class.DocUpdateMessageViewlet, core.space.Model, diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index a150a2947d6..aedb9a8b1c9 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -16,6 +16,7 @@ import activity, { type DocUpdateMessage } from '@hcengineering/activity' import core, { DOMAIN_MODEL_TX, + DOMAIN_TX, DOMAIN_STATUS, type Ref, type Status, @@ -38,16 +39,20 @@ import { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_TASK, migrateDefaultStatusesBase } from '@hcengineering/model-task' import tags from '@hcengineering/tags' import task from '@hcengineering/task' -import tracker, { +import { type Issue, + type IssueRelation, type IssueStatus, type Project, TimeReportDayType, trackerId } from '@hcengineering/tracker' +import view, { type ViewOptionModel } from '@hcengineering/view' import { classicIssueTaskStatuses } from '.' +import tracker from './plugin' import { DOMAIN_TRACKER } from './types' +import { ganttViewOptions, issuesOptions } from './viewlets' async function createDefaultProject (tx: TxOperations): Promise { const current = await tx.findOne(tracker.class.Project, { @@ -181,6 +186,74 @@ export async function migrateAddStartDate (client: MigrationClient): Promise { + const txOp = new TxOperations(client, core.account.System) + + const viewlets = await client.findAll(view.class.Viewlet, { + _id: tracker.viewlet.IssueGantt + }) + if (viewlets.length === 0) return + + const desiredOther = ganttViewOptions().other ?? [] + + for (const v of viewlets) { + const current = v.viewOptions ?? { groupBy: [], orderBy: [], other: [] } + const currentOther: ViewOptionModel[] = current.other ?? [] + const existingKeys = new Set(currentOther.map((o) => o.key)) + const missing = desiredOther.filter((o) => !existingKeys.has(o.key)) + if (missing.length === 0) continue + + await txOp.update(v, { + viewOptions: { + ...current, + other: [...currentOther, ...missing] + } + }) + } +} + +// SEARCH_VIEW_OPTIONS (showQuickModeSelector / searchScope / +// searchHighlight) are appended to issuesOptions(), which feeds the stored +// List (IssueList) and Kanban (IssueKanban) viewlet docs. As with the Gantt +// viewlet, builder.createDoc is idempotent, so existing workspaces never pick +// up the new `other` entries on upgrade. Merge the missing keys into both +// viewlets by `key` — mirrors addGanttPhase1ViewOptions and is idempotent, so +// a re-run (or a partially-migrated workspace) is a no-op. +async function addSearchViewOptions (client: MigrationUpgradeClient): Promise { + const txOp = new TxOperations(client, core.account.System) + + const targets = [ + { id: tracker.viewlet.IssueList, desired: issuesOptions(false).other ?? [] }, + { id: tracker.viewlet.IssueKanban, desired: issuesOptions(true).other ?? [] } + ] + + for (const t of targets) { + const viewlets = await client.findAll(view.class.Viewlet, { _id: t.id }) + for (const v of viewlets) { + const current = v.viewOptions ?? { groupBy: [], orderBy: [], other: [] } + const currentOther: ViewOptionModel[] = current.other ?? [] + const existingKeys = new Set(currentOther.map((o) => o.key)) + const missing = t.desired.filter((o) => !existingKeys.has(o.key)) + if (missing.length === 0) continue + + await txOp.update(v, { + viewOptions: { + ...current, + other: [...currentOther, ...missing] + } + }) + } + } +} + async function migrateDefaultStatuses (client: MigrationClient, logger: ModelLogger): Promise { const defaultTypeId = tracker.ids.ClassingProjectType const typeDescriptor = tracker.descriptors.ProjectType @@ -375,6 +448,66 @@ async function migrateIssueStatuses (client: MigrationClient): Promise { ) } +/** + * Activity-Log Remove-Detail Fix. + * + * Legacy IssueRelation removals went through `ops.removeDoc`, which emits + * a bare TxRemoveDoc without parent-issue attachment. The activity + * pipeline therefore wrote a DocUpdateMessage whose attachedTo is the + * relation itself, never showing up in the issue's activity feed. This + * migration re-attaches such DUMs to their parent issue by looking up the + * original TxCreateDoc of the now-removed IssueRelation. Best-effort: + * when the create-tx is missing (db compaction) the DUM still gets + * `updateCollection='relations'` so the activity feed at least shows + * "removed dependency" without the target detail. + * + * Idempotent — once a DUM has Issue-side attachment + `updateCollection`, + * the predicate skips it on subsequent runs. The predicate is a local + * 4-LOC copy of `isBrokenRelationDum` in + * `plugins/tracker-resources/src/components/gantt/lib/relation-activity-migration.ts` + * (cannot import across package layers — models/tracker is below + * tracker-resources). The helper module is the one with unit tests. + */ +export async function migrateRelationActivityAttachment (client: MigrationClient): Promise { + const issueClass = tracker.class.Issue + const dums = await client.find(DOMAIN_ACTIVITY, { + _class: activity.class.DocUpdateMessage, + objectClass: tracker.class.IssueRelation, + action: 'remove' + }) + if (dums.length === 0) return + for (const dum of dums) { + const isBroken = dum.attachedToClass !== issueClass || dum.updateCollection !== 'relations' + if (!isBroken) continue + // Find the create-tx for this relation. The relation objectId remains + // the same across the doc's whole life — that's the key we look up. + const createTxes = await client.find>( + DOMAIN_TX, + { + _class: core.class.TxCreateDoc, + objectId: dum.objectId as Ref + }, + { limit: 1 } + ) + const createTx = createTxes[0] + const patch: Partial = {} + if (createTx?.attachedTo !== undefined && createTx.attachedToClass !== undefined) { + patch.attachedTo = createTx.attachedTo + patch.attachedToClass = createTx.attachedToClass + patch.updateCollection = createTx.collection ?? 'relations' + } else { + // Placeholder: ensure the DUM at least shows up in the parent + // issue's feed by giving it the collection name; we leave + // attachedTo as-is (the runtime DocUpdateMessageObjectValue will + // still try buildRemovedDoc with objectId, which often succeeds + // even when the create-tx for the *DUM's* attachedTo is gone). + if (dum.updateCollection === 'relations') continue + patch.updateCollection = 'relations' + } + await client.update(DOMAIN_ACTIVITY, { _id: dum._id }, patch) + } +} + export const trackerOperation: MigrateOperation = { async preMigrate (client: MigrationClient, logger: ModelLogger, mode): Promise { await tryMigrate(mode, client, trackerId, [ @@ -414,6 +547,22 @@ export const trackerOperation: MigrateOperation = { state: 'gantt-add-startdate', mode: 'upgrade', func: migrateAddStartDate + }, + { + // Phase-2 working-days calendar. The property is optional and + // additive — every existing Project keeps `workingDaysConfig = + // undefined` (legacy calendar-day semantics). The migration entry + // exists only so the tracker state-tracker registers the schema + // version bump; no data is touched. + state: 'gantt-add-working-days-config', + mode: 'upgrade', + func: async () => {} + }, + { + // Activity-Log Remove-Detail Fix. + state: 'relation-activity-attached-v2', + mode: 'upgrade', + func: migrateRelationActivityAttachment } ]) }, @@ -425,6 +574,14 @@ export const trackerOperation: MigrateOperation = { const tx = new TxOperations(client, core.account.System) await createDefaults(tx) } + }, + { + state: 'add-gantt-phase1-view-options', + func: addGanttPhase1ViewOptions + }, + { + state: 'add-search-view-options', + func: addSearchViewOptions } ]) } diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index ac5d0e46018..89d904ac03f 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -71,6 +71,7 @@ export default mergeIds(trackerId, tracker, { IssueList: '' as Ref, IssueTemplateList: '' as Ref, IssueKanban: '' as Ref, + IssueGantt: '' as Ref, MilestoneList: '' as Ref, ComponentList: '' as Ref, ProjectList: '' as Ref, @@ -79,6 +80,8 @@ export default mergeIds(trackerId, tracker, { ids: { TrackerNotificationGroup: '' as Ref, AssigneeNotification: '' as Ref, + // Notification on Dependency-Shift. + DependencyShiftedNotification: '' as Ref, BaseProjectType: '' as Ref, GuestIssueClassPermission: '' as Ref, ModulePermissionGroup: '' as Ref, diff --git a/models/tracker/src/presenters.ts b/models/tracker/src/presenters.ts index dd609446758..66a7b037167 100644 --- a/models/tracker/src/presenters.ts +++ b/models/tracker/src/presenters.ts @@ -34,6 +34,22 @@ export function definePresenters (builder: Builder): void { presenter: tracker.component.NotificationIssuePresenter }) + // + // IssueRelation — single-line "FS +Nd → PROJ-29 Title" presenter so + // the activity feed shows which dependency was added or removed + // (instead of the generic "Dependency" class label). + // + builder.mixin(tracker.class.IssueRelation, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.IssueRelationPresenter + }) + + // + // Dependency-Shift bundle notification renderer. + // + builder.mixin(tracker.class.DependencyShiftedNotification, core.class.Class, view.mixin.ObjectPresenter, { + presenter: tracker.component.DependencyShiftedPresenter + }) + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.PreviewPresenter, { presenter: tracker.component.IssuePreview }) diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts index 249a592ef6d..853e19450ca 100644 --- a/models/tracker/src/types.ts +++ b/models/tracker/src/types.ts @@ -51,6 +51,7 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import core, { TAttachedDoc, TDoc, TStatus, TType } from '@hcengineering/model-core' +import notification, { TCommonInboxNotification } from '@hcengineering/model-notification' import task, { TTask, TProject as TTaskProject } from '@hcengineering/model-task' import { getEmbeddedLabel, type IntlString } from '@hcengineering/platform' import tags, { type TagElement } from '@hcengineering/tags' @@ -59,6 +60,8 @@ import { type ProjectTargetPreference, type Component, type DependencyKind, + type DependencyShiftedNotification, + type DependencyShiftRequest, type Issue, type IssueChildInfo, type IssueParentInfo, @@ -73,8 +76,10 @@ import { type RelatedClassRule, type RelatedIssueTarget, type RelatedSpaceRule, + type ShiftedIssuePayload, type TimeReportDayType, - type TimeSpendReport + type TimeSpendReport, + type WorkingDaysConfig } from '@hcengineering/tracker' import tracker from './plugin' import { type TaskType } from '@hcengineering/task' @@ -137,6 +142,9 @@ export class TProject extends TTaskProject implements Project { @Prop(Collection(tracker.class.RelatedIssueTarget), tracker.string.RelatedIssues) relatedIssueTargets!: number + + @Prop(TypeRecord(), tracker.string.WorkingDaysConfig) + workingDaysConfig?: WorkingDaysConfig } /** * @public @@ -242,6 +250,15 @@ export class TIssue extends TTask implements Issue { @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate) declare dueDate: Timestamp | null + // Soft deadline, independent of dueDate. Optional. + // When set, the Gantt renders a flag marker at this date and flags the + // issue as overdue when dueDate > deadline. Undefined for existing issues + // until the user opts in via the Issue editor (this change ships the + // inline ControlPanel field; a Gantt context-menu shortcut is a + // separate follow-up, see Out-of-scope section). + @Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.Deadline) + deadline?: Timestamp | null + @Prop(TypeRef(tracker.class.Milestone), tracker.string.Milestone, { icon: tracker.icon.Milestone }) @Index(IndexKind.Indexed) milestone!: Ref | null @@ -263,6 +280,19 @@ export class TIssue extends TTask implements Issue { @Prop(Collection(time.class.ToDo), getEmbeddedLabel('Action Items')) todos?: CollectionSize + + /** + * Auto-Scheduling-Toggle. + * + * Optional property so existing issues stay on the default cascade + * behaviour with no migration. `@Hidden` keeps the field out of the + * generic filter/sort UI in `getFiltredKeys`; the dedicated toggle in + * `ControlPanel.svelte` is the supported entry point. Cascade-time + * checks live in `gantt/lib/scheduler.ts` (Step 5b filter). + */ + @Prop(TypeString(), tracker.string.SchedulingMode) + @Hidden() + schedulingMode?: 'auto' | 'manual' } /** * @public @@ -391,6 +421,9 @@ export class TComponent extends TDoc implements Component { @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) attachments?: number + @Prop(TypeNumber(), tracker.string.Color) + color?: number + declare space: Ref } @@ -423,6 +456,9 @@ export class TMilestone extends TDoc implements Milestone { @Prop(TypeDate(), tracker.string.TargetDate) targetDate!: Timestamp + @Prop(TypeNumber(), tracker.string.Color) + color?: number + declare space: Ref } @@ -459,3 +495,64 @@ export class TClassicProjectTypeData extends TProject implements RolesAssignment @Mixin(tracker.mixin.IssueTypeData, tracker.class.Issue) @UX(getEmbeddedLabel('Issue'), tracker.icon.Issue) export class TIssueTypeData extends TIssue {} + +/** + * Notification on Dependency-Shift. + * + * Persisted model class for the cascade-shift bundle notification. Extends + * `CommonInboxNotification` so it inherits inbox/email/push routing for + * free; the cascade-specific payload lives in the (un-`@Prop`'d) fields + * which are still serialised as part of the Doc body — same pattern that + * `TReactionInboxNotification` uses for its `ref`/`emoji` fields. + * + * @public + */ +@Model(tracker.class.DependencyShiftedNotification, notification.class.CommonInboxNotification) +export class TDependencyShiftedNotification extends TCommonInboxNotification implements DependencyShiftedNotification { + @Prop(TypeRef(tracker.class.Issue), tracker.string.Issue) + triggerIssueId!: Ref + + @Prop(TypeString(), tracker.string.Issue) + triggerIssueIdentifier!: string + + @Prop(TypeString(), tracker.string.Issue) + triggerIssueTitle!: string + + triggerUserId!: AccountUuid + + shiftedIssues!: ShiftedIssuePayload[] + + @Prop(TypeString(), tracker.string.DependencyShifted) + cascadeToken!: string +} + +/** + * Notification on Dependency-Shift. + * + * Short-lived signal doc (DOMAIN_TRACKER) a Gantt client writes into the + * project space after a cascade commit. The `OnDependencyShiftRequest` server + * trigger consumes it, dispatches notifications privileged, and removes it. It + * deliberately carries no `triggerUserId` — the trigger derives the author + * from `tx.modifiedBy` (anti-spoofing). + * + * @public + */ +@Model(tracker.class.DependencyShiftRequest, core.class.Doc, DOMAIN_TRACKER) +export class TDependencyShiftRequest extends TDoc implements DependencyShiftRequest { + @Prop(TypeRef(tracker.class.Issue), tracker.string.Issue) + triggerIssueId!: Ref + + @Prop(TypeString(), tracker.string.Issue) + triggerIssueIdentifier!: string + + @Prop(TypeString(), tracker.string.Issue) + triggerIssueTitle!: string + + @Prop(TypeRef(tracker.class.Project), tracker.string.Project) + triggerIssueSpace!: Ref + + shiftedIssues!: ShiftedIssuePayload[] + + @Prop(TypeString(), tracker.string.DependencyShifted) + cascadeToken!: string +} diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts index 509e00d7d0e..30282c7ae08 100644 --- a/models/tracker/src/viewlets.ts +++ b/models/tracker/src/viewlets.ts @@ -23,6 +23,38 @@ import tags from '@hcengineering/tags' import { type ViewOptionModel, type BuildModelKey, type ViewOptionsModel } from '@hcengineering/view' import tracker from './plugin' +// Shared Customize-View knobs reused by both `issuesOptions()` +// (List + Kanban) and the Gantt viewlet block. Keep this list as the single +// source of truth so a change in one place can never disagree with the +// other half of the UI. +const SEARCH_VIEW_OPTIONS: ViewOptionModel[] = [ + { + key: 'showQuickModeSelector', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.ShowQuickModeSelector + }, + { + key: 'searchScope', + type: 'dropdown', + defaultValue: 'all', + values: [ + { id: 'title', label: tracker.string.SearchScopeTitle }, + { id: 'title-description', label: tracker.string.SearchScopeTitleDescription }, + { id: 'all', label: tracker.string.SearchScopeAll } + ], + label: tracker.string.SearchScopeLabel + }, + { + key: 'searchHighlight', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.SearchHighlight + } +] + export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ groupBy: [ 'status', @@ -75,7 +107,8 @@ export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({ action: view.function.HideArchived, label: view.string.HideArchived }, - ...(!kanban ? [showColorsViewOption] : []) + ...(!kanban ? [showColorsViewOption] : []), + ...SEARCH_VIEW_OPTIONS ] }) @@ -191,6 +224,20 @@ export function issueConfig ( displayProps: { key: key + 'dueDate', compression: true }, props: { kind: 'list' } }, + { + // Predecessors column. Opt-in via "Configure columns" + // (displayProps.optional = true) because the vast majority of issues + // have zero predecessors and the cell would otherwise be permanently + // empty in a default list. Renders the upstream Issue identifier + // followed by kind+lag, e.g. "PROJ-3 FS+2d"; multiple predecessors + // collapse to "first +N more" with a hover tooltip showing every + // dependency. Sortable is intentionally NOT enabled — there is no + // meaningful total order across predecessor lists. + key: '', + label: tracker.string.Predecessors, + presenter: tracker.component.PredecessorsColumn, + displayProps: { key: key + 'predecessors', optional: true } + }, { key: '', label: tracker.string.Estimation, @@ -212,6 +259,306 @@ export function issueConfig ( ] } +export function ganttViewOptions (): ViewOptionsModel { + // The Gantt viewlet deliberately leaves the generic `groupBy` list empty and + // exposes its own `ganttGroupBy` toggle below instead: the canvas groups into + // swimlanes itself, which the shared grouped-list machinery cannot render. + // The sidebar-column toggles below are wired up to GanttSidebar. + return { + groupBy: [], + orderBy: [ + ['startDate', SortingOrder.Ascending], + ['rank', SortingOrder.Ascending], + ['dueDate', SortingOrder.Ascending] + ], + other: [ + { + key: 'ganttShowIssueCode', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttShowIssueCode + }, + { + key: 'ganttShowTitle', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttShowTitle + }, + { + key: 'ganttShowStatus', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttShowStatus + }, + { + // Default-on safety prompt: when set, dragging an issue's bar to a + // new date range shows a confirm dialog before writing the change. + // Easy to misclick a bar while panning, + // and a one-click confirm prevents accidental schedule edits. + key: 'ganttConfirmMove', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttConfirmMove + }, + { + // Same idea but for left/right resize handles. + key: 'ganttConfirmResize', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttConfirmResize + }, + { + // Sidebar column showing predecessor notation (e.g. "12FS+2d"). + // Hidden by default so existing users don't see a new column appear. + // Toggling on requires no migration — the column is purely derived + // from the IssueRelation collection. + key: 'ganttShowPredecessors', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttShowPredecessors + }, + { + // Critical-path overlay toggle. When on, critical bars get a red + // border and fill overlay; critical relations get red arrows; non-critical + // bars show a grey slack glyph. + key: 'ganttCriticalPath', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.CriticalPathOn + }, + { + // Slack column in the sidebar. Shows numeric slack days or "CP" badge + // for critical issues. Requires ganttCriticalPath to be meaningful. + key: 'ganttSlackColumn', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.SlackColumn + }, + { + // Bar label slot (left of bar). + // Default 'none' so existing users see no left-side label. + key: 'ganttBarLabelLeft', + type: 'dropdown', + defaultValue: 'none', + actionTarget: 'display', + label: tracker.string.GanttBarLabelLeft, + values: [ + { id: 'none', label: tracker.string.BarLabelNone }, + { id: 'title', label: tracker.string.BarLabelTitle }, + { id: 'identifier', label: tracker.string.BarLabelIdentifier }, + { id: 'assignee', label: tracker.string.BarLabelAssignee }, + { id: 'priority', label: tracker.string.BarLabelPriority }, + { id: 'status', label: tracker.string.BarLabelStatus }, + { id: 'estimation', label: tracker.string.BarLabelEstimation }, + { id: 'progress', label: tracker.string.BarLabelProgress } + ] + }, + { + // Bar label slot (inside bar, rendered only if bar > 60px wide). + // Default 'title' preserves the legacy in-bar title rendering. + key: 'ganttBarLabelInside', + type: 'dropdown', + defaultValue: 'title', + actionTarget: 'display', + label: tracker.string.GanttBarLabelInside, + values: [ + { id: 'none', label: tracker.string.BarLabelNone }, + { id: 'title', label: tracker.string.BarLabelTitle }, + { id: 'identifier', label: tracker.string.BarLabelIdentifier }, + { id: 'assignee', label: tracker.string.BarLabelAssignee }, + { id: 'priority', label: tracker.string.BarLabelPriority }, + { id: 'status', label: tracker.string.BarLabelStatus }, + { id: 'estimation', label: tracker.string.BarLabelEstimation }, + { id: 'progress', label: tracker.string.BarLabelProgress } + ] + }, + { + // Bar label slot (right of bar). + // Default 'none'. + key: 'ganttBarLabelRight', + type: 'dropdown', + defaultValue: 'none', + actionTarget: 'display', + label: tracker.string.GanttBarLabelRight, + values: [ + { id: 'none', label: tracker.string.BarLabelNone }, + { id: 'title', label: tracker.string.BarLabelTitle }, + { id: 'identifier', label: tracker.string.BarLabelIdentifier }, + { id: 'assignee', label: tracker.string.BarLabelAssignee }, + { id: 'priority', label: tracker.string.BarLabelPriority }, + { id: 'status', label: tracker.string.BarLabelStatus }, + { id: 'estimation', label: tracker.string.BarLabelEstimation }, + { id: 'progress', label: tracker.string.BarLabelProgress } + ] + }, + { + // Opt-in for the Quick-Info popover. + // false (default = legacy) = single-click only selects + focuses + // the bar (no popup, no editor); double-click on the bar opens + // the full editor as before. No behaviour change for existing users. + // true = single-click selects + focuses AND opens the lightweight + // Quick-Info popover. Double-click continues to open the full + // editor on top of the popover. + key: 'ganttQuickInfoOnClick', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttQuickInfoOnClick + }, + { + // Extended sidebar grid. When on, the sidebar renders a sortable + // header row + per-column cells (identifier, title, predecessors, + // slack, plus any toggled columns below). When off, the legacy + // compact layout is preserved. + key: 'ganttSidebarColumnsExtended', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarColumnsExtended + }, + // Per-column visibility toggles for the extended sidebar. Hidden when + // ganttSidebarColumnsExtended is off. Identifier + Title + Predecessors + // + Slack are always shown (default column set). + { + key: 'ganttSidebarShowStatus', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowStatus, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowPriority', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowPriority, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowAssignee', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowAssignee, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowEstimation', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowEstimation, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowStartDate', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowStartDate, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowDueDate', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowDueDate, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowDeadline', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowDeadline, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + key: 'ganttSidebarShowProgress', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttSidebarShowProgress, + dependsOn: 'ganttSidebarColumnsExtended' + }, + { + // Group-by swimlanes. Issues group into horizontal lanes + // by status/priority/assignee/component/milestone/label. Default + // 'none' preserves the legacy hierarchy view bit-for-bit. The + // sidebar shows a chevron + label + count header per lane; the + // canvas paints a tint band behind each header. + key: 'ganttGroupBy', + type: 'dropdown', + defaultValue: 'none', + values: [ + { id: 'none', label: tracker.string.GanttGroupByNone }, + { id: 'status', label: tracker.string.GanttGroupByStatus }, + { id: 'priority', label: tracker.string.GanttGroupByPriority }, + { id: 'assignee', label: tracker.string.GanttGroupByAssignee }, + { id: 'component', label: tracker.string.GanttGroupByComponent }, + { id: 'milestone', label: tracker.string.GanttGroupByMilestone }, + { id: 'label', label: tracker.string.GanttGroupByLabel } + ], + actionTarget: 'display', + label: tracker.string.GanttGroupBy + }, + { + // Overlay toggles. When past-due is on, overdue bars + // get a diagonal red-stripe overlay; when blocked is on, bars with + // an unsatisfied predecessor get a hatch overlay. Both default ON + // so new users immediately see the visual cues. + key: 'ganttShowPastDueOverlay', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttShowPastDueOverlay + }, + { + key: 'ganttShowBlockedOverlay', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttShowBlockedOverlay + }, + { + // Sub-issue progress fill: when ON, parent bars show a secondary + // fill proportional to child-issue completion. Default OFF to keep + // the chart uncluttered for teams that don't use sub-issues. + key: 'ganttShowSubIssueProgress', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttShowSubIssueProgress + }, + // Plan-2 — search-scope / quick-filter / highlight toggles. + ...SEARCH_VIEW_OPTIONS + ] + } +} + +export function ganttConfig (): BuildModelKey[] { + // Minimal config — Gantt drives its own column layout. + return [ + { + key: '', + presenter: tracker.component.PriorityEditor, + label: tracker.string.Priority, + props: { kind: 'list', size: 'small' } + }, + { key: '', presenter: tracker.component.IssuePresenter, label: tracker.string.Issue } + ] +} + export function defineViewlets (builder: Builder): void { builder.createDoc( view.class.ViewletDescriptor, @@ -224,6 +571,17 @@ export function defineViewlets (builder: Builder): void { tracker.viewlet.Kanban ) + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: tracker.string.Gantt, + icon: tracker.icon.Gantt, + component: tracker.component.GanttView + }, + tracker.viewlet.Gantt + ) + builder.createDoc( view.class.Viewlet, core.space.Model, @@ -501,6 +859,23 @@ export function defineViewlets (builder: Builder): void { tracker.viewlet.IssueKanban ) + // Gantt is registered AFTER List + Kanban so List remains the default + // viewlet (ViewletSelector falls back to viewlets[0] when no preference + // is saved). Putting Gantt last avoids surprising users with an empty + // canvas on first visit. + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: tracker.viewlet.Gantt, + viewOptions: ganttViewOptions(), + configOptions: { strict: true, hiddenKeys: ['title'] }, + config: ganttConfig() + }, + tracker.viewlet.IssueGantt + ) + const componentListViewOptions: ViewOptionsModel = { groupBy: ['lead', 'createdBy', 'modifiedBy'], orderBy: [ diff --git a/packages/gantt/.eslintrc.js b/packages/gantt/.eslintrc.js new file mode 100644 index 00000000000..72235dc2833 --- /dev/null +++ b/packages/gantt/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/packages/gantt/jest.config.js b/packages/gantt/jest.config.js new file mode 100644 index 00000000000..6479224b481 --- /dev/null +++ b/packages/gantt/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ['./src'], + coverageReporters: ['text-summary', 'html'] +} diff --git a/packages/gantt/package.json b/packages/gantt/package.json new file mode 100644 index 00000000000..a2aad0c3283 --- /dev/null +++ b/packages/gantt/package.json @@ -0,0 +1,57 @@ +{ + "name": "@hcengineering/gantt", + "version": "0.7.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "src/**/*", + "!src/**/__tests__/**", + "tsconfig.json" + ], + "author": "Hardcore Engineering Inc.", + "license": "EPL-2.0", + "scripts": { + "_phase:build": "compile transpile src", + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "build": "compile", + "test": "jest --passWithNoTests --silent", + "build:watch": "compile", + "format": "format src" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.21", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "eslint": "^8.54.0", + "prettier": "^3.6.2", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "typescript": "^5.9.3", + "eslint-plugin-svelte": "^2.35.1" + }, + "dependencies": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/hcengineering/platform.git" + }, + "publishConfig": { + "access": "public" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/packages/gantt/src/__tests__/breakpoint.test.ts b/packages/gantt/src/__tests__/breakpoint.test.ts new file mode 100644 index 00000000000..83e261857b0 --- /dev/null +++ b/packages/gantt/src/__tests__/breakpoint.test.ts @@ -0,0 +1,57 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { detectLayoutMode, isPhone, isTablet, isDesktop, PHONE_MAX_WIDTH, TABLET_MAX_WIDTH } from '../breakpoint' + +describe('breakpoint — detectLayoutMode', () => { + it('classifies very small widths as phone', () => { + expect(detectLayoutMode(320)).toBe('phone') + expect(detectLayoutMode(390)).toBe('phone') + expect(detectLayoutMode(640)).toBe('phone') + }) + + it('classifies mid widths as tablet', () => { + expect(detectLayoutMode(641)).toBe('tablet') + expect(detectLayoutMode(768)).toBe('tablet') + expect(detectLayoutMode(1024)).toBe('tablet') + }) + + it('classifies large widths as desktop', () => { + expect(detectLayoutMode(1025)).toBe('desktop') + expect(detectLayoutMode(1440)).toBe('desktop') + expect(detectLayoutMode(3840)).toBe('desktop') + }) + + it('treats degenerate widths as phone (safest read-only)', () => { + expect(detectLayoutMode(0)).toBe('phone') + expect(detectLayoutMode(-100)).toBe('phone') + expect(detectLayoutMode(Number.NaN)).toBe('phone') + }) + + it('exposes the documented breakpoint constants', () => { + expect(PHONE_MAX_WIDTH).toBe(640) + expect(TABLET_MAX_WIDTH).toBe(1024) + }) +}) + +describe('breakpoint — predicates', () => { + it('isPhone is true only on phone mode', () => { + expect(isPhone('phone')).toBe(true) + expect(isPhone('tablet')).toBe(false) + expect(isPhone('desktop')).toBe(false) + }) + + it('isTablet is true only on tablet mode', () => { + expect(isTablet('phone')).toBe(false) + expect(isTablet('tablet')).toBe(true) + expect(isTablet('desktop')).toBe(false) + }) + + it('isDesktop is true only on desktop mode', () => { + expect(isDesktop('phone')).toBe(false) + expect(isDesktop('tablet')).toBe(false) + expect(isDesktop('desktop')).toBe(true) + }) +}) diff --git a/packages/gantt/src/__tests__/cascade-popup-layout.test.ts b/packages/gantt/src/__tests__/cascade-popup-layout.test.ts new file mode 100644 index 00000000000..d6c1a96ac9c --- /dev/null +++ b/packages/gantt/src/__tests__/cascade-popup-layout.test.ts @@ -0,0 +1,48 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { computeCascadeBodyHeight } from '../cascade-popup-layout' + +const baseInput = { + rowHeight: 22, + barTopPadding: 32, + bodyVerticalPadding: 8, + bodyBottomSafety: 8, + bodyMaxHeight: 360 +} + +describe('cascade-popup-layout: computeCascadeBodyHeight', () => { + it('reserves padding + safety on top of the timeline content', () => { + // 3 rows × 22 = 66 timeline + 32 header pad = 98 svg height. Plus + // 8+8 = 16 padding/safety → 114. Crucially > 98 (the bug + // produced exactly 98 and clipped the last 8 px of the last row). + expect(computeCascadeBodyHeight({ ...baseInput, rowCount: 3 })).toBe(114) + }) + + it('returns a single-row tall enough to show the bar plus padding', () => { + expect(computeCascadeBodyHeight({ ...baseInput, rowCount: 1 })).toBe(70) + }) + + it('caps at bodyMaxHeight once content overflows', () => { + // 100 rows × 22 + extras = 2248 → clamps to 360. + expect(computeCascadeBodyHeight({ ...baseInput, rowCount: 100 })).toBe(360) + }) + + it('exact-fit on the cap boundary', () => { + expect(computeCascadeBodyHeight({ ...baseInput, rowCount: 15 })).toBe(360) + }) + + it('zero rows still reserves the header pad + padding (defensive)', () => { + expect(computeCascadeBodyHeight({ ...baseInput, rowCount: 0 })).toBe(48) + }) + + it('regression: pre-fix formula (no padding) would have clipped 3-row case', () => { + // Sanity check on the constant the bug fix reasoning depends on. + const preFix = 3 * 22 + 32 // 98 — the value that clipped in + const postFix = computeCascadeBodyHeight({ ...baseInput, rowCount: 3 }) + expect(postFix).toBeGreaterThan(preFix) + expect(postFix - preFix).toBeGreaterThanOrEqual(8) + }) +}) diff --git a/packages/gantt/src/__tests__/cascade-token.test.ts b/packages/gantt/src/__tests__/cascade-token.test.ts new file mode 100644 index 00000000000..08a6fff9f39 --- /dev/null +++ b/packages/gantt/src/__tests__/cascade-token.test.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { newCascadeToken } from '../cascade-token' + +describe('newCascadeToken — default prefix', () => { + it('uses the gantt-cascade prefix when no argument is given', () => { + const token = newCascadeToken() + expect(token.startsWith('gantt-cascade:')).toBe(true) + }) + + it('emits a colon separator between prefix and suffix', () => { + const token = newCascadeToken() + const segments = token.split(':') + expect(segments.length).toBe(2) + expect(segments[0]).toBe('gantt-cascade') + expect(segments[1].length).toBeGreaterThan(0) + }) +}) + +describe('newCascadeToken — custom prefix', () => { + it('uses the supplied prefix verbatim', () => { + expect(newCascadeToken('gantt-cascade-bypass').startsWith('gantt-cascade-bypass:')).toBe(true) + expect(newCascadeToken('gantt-bulk-drag').startsWith('gantt-bulk-drag:')).toBe(true) + expect(newCascadeToken('gantt-no-cascade').startsWith('gantt-no-cascade:')).toBe(true) + }) + + it('preserves prefix even when it contains its own dashes/digits', () => { + expect(newCascadeToken('scope-7-x').startsWith('scope-7-x:')).toBe(true) + }) +}) + +describe('newCascadeToken — suffix format', () => { + it('produces a - suffix with numeric segments', () => { + const token = newCascadeToken() + const suffix = token.slice('gantt-cascade:'.length) + expect(suffix).toMatch(/^\d+-\d+$/) + }) + + it('uses the current epoch millis for the leading suffix segment', () => { + const before = Date.now() + const token = newCascadeToken() + const after = Date.now() + const stamp = Number(token.slice('gantt-cascade:'.length).split('-')[0]) + expect(stamp).toBeGreaterThanOrEqual(before) + expect(stamp).toBeLessThanOrEqual(after) + }) +}) + +describe('newCascadeToken — uniqueness', () => { + it('emits a different token on every call within the same tick', () => { + const a = newCascadeToken() + const b = newCascadeToken() + const c = newCascadeToken() + expect(a).not.toBe(b) + expect(b).not.toBe(c) + expect(a).not.toBe(c) + }) + + it('emits distinct tokens across 1000 rapid calls', () => { + const seen = new Set() + for (let i = 0; i < 1000; i++) seen.add(newCascadeToken('bulk')) + expect(seen.size).toBe(1000) + }) +}) + +describe('newCascadeToken — counter monotonicity within a tick', () => { + it('increments the trailing counter segment on consecutive calls', () => { + const counterOf = (t: string): number => Number(t.split('-').pop() ?? '0') + const a = counterOf(newCascadeToken()) + const b = counterOf(newCascadeToken()) + const c = counterOf(newCascadeToken()) + expect(b).toBe(a + 1) + expect(c).toBe(b + 1) + }) +}) diff --git a/packages/gantt/src/__tests__/confirm-gate.test.ts b/packages/gantt/src/__tests__/confirm-gate.test.ts new file mode 100644 index 00000000000..acd7cfa30a5 --- /dev/null +++ b/packages/gantt/src/__tests__/confirm-gate.test.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { isConfirming, setConfirming, resetConfirmGate } from '../confirm-gate' + +describe('confirm-gate', () => { + beforeEach(() => { + resetConfirmGate() + }) + + it('defaults to false on first read', () => { + expect(isConfirming()).toBe(false) + }) + + it('returns true after setConfirming(true)', () => { + setConfirming(true) + expect(isConfirming()).toBe(true) + }) + + it('returns false after setConfirming(false)', () => { + setConfirming(true) + setConfirming(false) + expect(isConfirming()).toBe(false) + }) + + it('idempotent set true', () => { + setConfirming(true) + setConfirming(true) + expect(isConfirming()).toBe(true) + }) + + it('reset helper clears the flag', () => { + setConfirming(true) + resetConfirmGate() + expect(isConfirming()).toBe(false) + }) +}) diff --git a/packages/gantt/src/__tests__/cp-scheduler.test.ts b/packages/gantt/src/__tests__/cp-scheduler.test.ts new file mode 100644 index 00000000000..e2c672f2635 --- /dev/null +++ b/packages/gantt/src/__tests__/cp-scheduler.test.ts @@ -0,0 +1,74 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { DebouncedRecompute } from '../cp-scheduler' + +describe('DebouncedRecompute', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + afterEach(() => { + jest.useRealTimers() + }) + + it('runs once after the delay', () => { + const run = jest.fn() + const d = new DebouncedRecompute(200) + d.schedule(run) + expect(run).not.toHaveBeenCalled() + jest.advanceTimersByTime(199) + expect(run).not.toHaveBeenCalled() + jest.advanceTimersByTime(1) + expect(run).toHaveBeenCalledTimes(1) + expect(d.pending).toBe(false) + }) + + it('collapses a burst into a single run with the latest callback', () => { + const first = jest.fn() + const last = jest.fn() + const d = new DebouncedRecompute(200) + d.schedule(first) + jest.advanceTimersByTime(100) + d.schedule(first) + jest.advanceTimersByTime(100) + d.schedule(last) + jest.advanceTimersByTime(200) + expect(first).not.toHaveBeenCalled() + expect(last).toHaveBeenCalledTimes(1) + }) + + it('never runs a cancelled recompute — the both-toggles-off case', () => { + const run = jest.fn() + const d = new DebouncedRecompute(200) + d.schedule(run) + expect(d.pending).toBe(true) + // User switches the critical-path overlay AND the slack column off before + // the debounce elapses: the queued pass must not fire into a view that + // renders neither, nor raise the cycle warning banner. + d.cancel() + expect(d.pending).toBe(false) + jest.advanceTimersByTime(10_000) + expect(run).not.toHaveBeenCalled() + }) + + it('cancel is idempotent and safe when nothing is pending', () => { + const d = new DebouncedRecompute(200) + expect(() => { + d.cancel() + d.cancel() + }).not.toThrow() + expect(d.pending).toBe(false) + }) + + it('can be re-armed after a cancel', () => { + const run = jest.fn() + const d = new DebouncedRecompute(200) + d.schedule(run) + d.cancel() + d.schedule(run) + jest.advanceTimersByTime(200) + expect(run).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/gantt/src/__tests__/drag-controller.test.ts b/packages/gantt/src/__tests__/drag-controller.test.ts new file mode 100644 index 00000000000..8713bfab79b --- /dev/null +++ b/packages/gantt/src/__tests__/drag-controller.test.ts @@ -0,0 +1,618 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { reduce } from '../drag-controller' +import { createTimeScale, snapToUtcMidnight } from '../time-scale' +import type { DragState, DragTarget, GanttItem } from '../types' + +// Neutral stand-in for a tracker Issue/Milestone: the reducer only reads `_id`, +// so the date/space fields are plain padding consumed by the test assertions. +interface TestItem extends GanttItem { + _class?: string + space?: string + label?: string + startDate?: number | null + dueDate?: number | null + targetDate?: number + status?: number + comments?: number + parents?: unknown[] +} + +const ts = createTimeScale('week', Date.UTC(2026, 0, 1)) +const issueRef = 'issue-1' + +const issue: TestItem = { + _id: 'issue-1', + _class: 'tracker:class:Issue', + space: 'space-1', + startDate: Date.UTC(2026, 0, 5), + dueDate: Date.UTC(2026, 0, 12) + // The reducer only touches startDate/dueDate; the rest is type-padding. +} + +const issueTarget: DragTarget = { kind: 'issue', doc: issue } + +describe('drag-controller — idle transitions', () => { + const idle: DragState = { kind: 'idle' } + + it('mouseenter-bar moves idle → hover-bar', () => { + const next = reduce(idle, { type: 'mouseenter-bar', issueId: issueRef, edge: 'body' }, ts) + expect(next).toEqual({ kind: 'hover-bar', issueId: issueRef, edge: 'body' }) + }) + + it('mouseleave-bar stays idle when already idle', () => { + const next = reduce(idle, { type: 'mouseleave-bar' }, ts) + expect(next).toEqual(idle) + }) + + it('mousemove stays idle when no drag is active', () => { + const next = reduce(idle, { type: 'mousemove', cursorX: 100 }, ts) + expect(next).toEqual(idle) + }) + + it('mouseup stays idle when no drag is active', () => { + const next = reduce(idle, { type: 'mouseup' }, ts) + expect(next).toEqual(idle) + }) +}) + +describe('drag-controller — body drag', () => { + it('mousedown-bar on edge=body transitions hover → dragging-body', () => { + const hover: DragState = { kind: 'hover-bar', issueId: issue._id, edge: 'body' } + const next = reduce( + hover, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'body', + cursorX: 200 + }, + ts + ) + expect(next.kind).toBe('dragging-body') + if (next.kind !== 'dragging-body') return + expect(next.target.doc._id).toBe(issue._id) + expect(next.originStart).toBe(issue.startDate) + expect(next.originEnd).toBe(issue.dueDate) + expect(next.cursorStartX).toBe(200) + expect(next.previewStart).toBe(issue.startDate) + expect(next.previewEnd).toBe(issue.dueDate) + }) + + it('mousemove shifts both preview dates by snapped delta', () => { + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: issue.startDate as number, + previewEnd: issue.dueDate as number + } + // Week-zoom = 14 px/day → 28 px = 2 days + const next = reduce(dragging, { type: 'mousemove', cursorX: 228 }, ts) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.previewStart).toBe((issue.startDate as number) + 2 * 86_400_000) + expect(next.previewEnd).toBe((issue.dueDate as number) + 2 * 86_400_000) + }) + + it('mouseup returns dragging-body → idle', () => { + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: issue.startDate as number, + previewEnd: issue.dueDate as number + } + const next = reduce(dragging, { type: 'mouseup' }, ts) + expect(next).toEqual({ kind: 'idle' }) + }) + + it('cancel returns dragging-body → idle without applying the move', () => { + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: (issue.startDate as number) + 5 * 86_400_000, + previewEnd: (issue.dueDate as number) + 5 * 86_400_000 + } + const next = reduce(dragging, { type: 'cancel' }, ts) + expect(next).toEqual({ kind: 'idle' }) + }) +}) + +describe('drag-controller — resize-left', () => { + it('mousedown-bar on edge=left transitions hover → resizing-left', () => { + const hover: DragState = { kind: 'hover-bar', issueId: issue._id, edge: 'left' } + const next = reduce( + hover, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'left', + cursorX: 50 + }, + ts + ) + expect(next.kind).toBe('resizing-left') + if (next.kind !== 'resizing-left') return + expect(next.previewStart).toBe(issue.startDate) + expect(next.originEnd).toBe(issue.dueDate) + }) + + it('resize-left mousemove updates previewStart but not originEnd', () => { + const resizing: DragState = { + kind: 'resizing-left', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 50, + previewStart: issue.startDate as number + } + // 14 px = 1 day at week zoom + const next = reduce(resizing, { type: 'mousemove', cursorX: 64 }, ts) + if (next.kind !== 'resizing-left') throw new Error('expected resizing-left') + expect(next.previewStart).toBe((issue.startDate as number) + 1 * 86_400_000) + }) + + it('resize-left clamps previewStart to be ≤ originEnd', () => { + const resizing: DragState = { + kind: 'resizing-left', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 50, + previewStart: issue.startDate as number + } + // Move 100 days right — past the due date + const next = reduce(resizing, { type: 'mousemove', cursorX: 50 + 100 * 14 }, ts) + if (next.kind !== 'resizing-left') throw new Error('expected resizing-left') + expect(next.previewStart).toBe(issue.dueDate) + }) +}) + +describe('drag-controller — resize-right', () => { + it('mousedown-bar on edge=right transitions hover → resizing-right', () => { + const hover: DragState = { kind: 'hover-bar', issueId: issue._id, edge: 'right' } + const next = reduce( + hover, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'right', + cursorX: 250 + }, + ts + ) + expect(next.kind).toBe('resizing-right') + if (next.kind !== 'resizing-right') return + expect(next.previewEnd).toBe(issue.dueDate) + expect(next.originStart).toBe(issue.startDate) + }) + + it('resize-right mousemove updates previewEnd but not originStart', () => { + const resizing: DragState = { + kind: 'resizing-right', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 250, + previewEnd: issue.dueDate as number + } + const next = reduce(resizing, { type: 'mousemove', cursorX: 264 }, ts) + if (next.kind !== 'resizing-right') throw new Error('expected resizing-right') + expect(next.previewEnd).toBe((issue.dueDate as number) + 1 * 86_400_000) + }) + + it('resize-right clamps previewEnd to be ≥ originStart', () => { + const resizing: DragState = { + kind: 'resizing-right', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 250, + previewEnd: issue.dueDate as number + } + // Move 100 days left — past the start + const next = reduce(resizing, { type: 'mousemove', cursorX: 250 - 100 * 14 }, ts) + if (next.kind !== 'resizing-right') throw new Error('expected resizing-right') + expect(next.previewEnd).toBe(issue.startDate) + }) +}) + +describe('drag-controller — unscheduled drag', () => { + const undated: TestItem = { + _id: 'u', + parents: [], + startDate: null, + dueDate: null + } + const undatedTarget: DragTarget = { kind: 'issue', doc: undated } + + it('mousedown-unscheduled from idle transitions to dragging-unscheduled with origin fields', () => { + const next = reduce({ kind: 'idle' }, { type: 'mousedown-unscheduled', target: undatedTarget, cursorX: 100 }, ts) + expect(next.kind).toBe('dragging-unscheduled') + if (next.kind !== 'dragging-unscheduled') return + expect(next.previewStart).toBeGreaterThan(0) + expect(next.previewEnd).toBe(next.previewStart + 86_400_000) // default 1-day span + // Origin fields are populated so commitDrag/overlay treat unscheduled like + // a regular drag with an implicit "today" anchor. + expect(next.originStart).toBe(next.previewStart) + expect(next.originEnd).toBe(next.previewEnd) + // Guard against click-without-drag scheduling to today: hasCanvasTarget + // is false until a real canvas-X is seen on mousemove. + expect(next.hasCanvasTarget).toBe(false) + }) + + it('mousemove without canvasX keeps hasCanvasTarget false', () => { + const start: DragState = { + kind: 'dragging-unscheduled', + target: undatedTarget, + originStart: 1_700_000_000_000, + originEnd: 1_700_000_000_000 + 86_400_000, + cursorStartX: 100, + previewStart: 1_700_000_000_000, + previewEnd: 1_700_000_000_000 + 86_400_000, + hasCanvasTarget: false + } + const next = reduce(start, { type: 'mousemove', cursorX: 200 }, ts) + if (next.kind !== 'dragging-unscheduled') throw new Error('expected dragging-unscheduled') + expect(next.hasCanvasTarget).toBe(false) + expect(next.previewStart).toBe(start.previewStart) + }) + + it('mousemove with canvasX flips hasCanvasTarget true and snaps previewStart', () => { + const start: DragState = { + kind: 'dragging-unscheduled', + target: undatedTarget, + originStart: 1_700_000_000_000, + originEnd: 1_700_000_000_000 + 86_400_000, + cursorStartX: 100, + previewStart: 1_700_000_000_000, + previewEnd: 1_700_000_000_000 + 86_400_000, + hasCanvasTarget: false + } + // canvasX = 7 * pxPerDay (week zoom = 14 px/day) → 7 days past origin + const next = reduce(start, { type: 'mousemove', cursorX: 200, canvasX: 7 * 14 }, ts) + if (next.kind !== 'dragging-unscheduled') throw new Error('expected dragging-unscheduled') + expect(next.hasCanvasTarget).toBe(true) + expect(next.previewStart).toBe(snapToUtcMidnight(ts.fromX(7 * 14))) + expect(next.previewEnd).toBe(next.previewStart + 86_400_000) + }) +}) + +describe('drag-controller — direct idle → drag (Playwright + edge-case)', () => { + it('mousedown-bar from idle transitions directly to dragging-body', () => { + const next = reduce( + { kind: 'idle' }, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'body', + cursorX: 200 + }, + ts + ) + expect(next.kind).toBe('dragging-body') + if (next.kind !== 'dragging-body') return + expect(next.previewStart).toBe(issue.startDate) + expect(next.previewEnd).toBe(issue.dueDate) + expect(next.cursorStartX).toBe(200) + }) + + it('mousedown-bar (edge=left) from idle goes directly to resizing-left', () => { + const next = reduce( + { kind: 'idle' }, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'left', + cursorX: 50 + }, + ts + ) + expect(next.kind).toBe('resizing-left') + }) +}) + +describe('drag-controller — connector states', () => { + const source: TestItem = { + _id: 'S', + _class: 'tracker:class:Issue', + space: 'sp', + startDate: 0, + dueDate: 86_400_000 + } + const target: TestItem = { + _id: 'T', + _class: 'tracker:class:Issue', + space: 'sp', + startDate: 86_400_000, + dueDate: 86_400_000 * 2 + } + + it('idle + mousedown-connector → connector-drawing', () => { + const next = reduce( + { kind: 'idle' }, + { type: 'mousedown-connector', source, originPx: { x: 100, y: 50 }, cursorPx: { x: 100, y: 50 } }, + ts + ) + expect(next.kind).toBe('connector-drawing') + if (next.kind !== 'connector-drawing') return + expect(next.source._id).toBe(source._id) + expect(next.originPx).toEqual({ x: 100, y: 50 }) + expect(next.cursorPx).toEqual({ x: 100, y: 50 }) + }) + + it('connector-drawing + mousemove with no target stays connector-drawing, updates cursorPx', () => { + const drawing: DragState = { + kind: 'connector-drawing', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 100, y: 50 } + } + const next = reduce(drawing, { type: 'mousemove-connector', cursorPx: { x: 250, y: 80 }, hoveredBar: null }, ts) + expect(next.kind).toBe('connector-drawing') + if (next.kind !== 'connector-drawing') return + expect(next.cursorPx).toEqual({ x: 250, y: 80 }) + }) + + it('connector-drawing + mousemove over a bar → connector-target-hover', () => { + const drawing: DragState = { + kind: 'connector-drawing', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 100, y: 50 } + } + const next = reduce(drawing, { type: 'mousemove-connector', cursorPx: { x: 300, y: 80 }, hoveredBar: target }, ts) + expect(next.kind).toBe('connector-target-hover') + if (next.kind !== 'connector-target-hover') return + expect(next.target._id).toBe(target._id) + expect(next.cursorPx).toEqual({ x: 300, y: 80 }) + }) + + it('connector-target-hover + mousemove off any bar → back to connector-drawing', () => { + const hover: DragState = { + kind: 'connector-target-hover', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 300, y: 80 }, + target + } + const next = reduce(hover, { type: 'mousemove-connector', cursorPx: { x: 400, y: 90 }, hoveredBar: null }, ts) + expect(next.kind).toBe('connector-drawing') + expect('target' in next).toBe(false) + }) + + it('source bar itself does NOT become a valid target (self-hover stays drawing)', () => { + const drawing: DragState = { + kind: 'connector-drawing', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 110, y: 55 } + } + const next = reduce(drawing, { type: 'mousemove-connector', cursorPx: { x: 120, y: 55 }, hoveredBar: source }, ts) + expect(next.kind).toBe('connector-drawing') + expect('target' in next).toBe(false) + }) + + it('connector-drawing + mouseup → idle', () => { + const drawing: DragState = { + kind: 'connector-drawing', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 250, y: 80 } + } + expect(reduce(drawing, { type: 'mouseup-connector' }, ts)).toEqual({ kind: 'idle' }) + }) + + it('connector-drawing + cancel → idle', () => { + const drawing: DragState = { + kind: 'connector-drawing', + source, + originPx: { x: 100, y: 50 }, + cursorPx: { x: 250, y: 80 } + } + expect(reduce(drawing, { type: 'cancel' }, ts)).toEqual({ kind: 'idle' }) + }) +}) + +describe('drag-controller — milestone target', () => { + const milestone: TestItem = { + _id: 'ms-1', + _class: 'tracker:class:Milestone', + space: 'space-1', + label: 'Sprint 1', + startDate: Date.UTC(2026, 0, 5), + targetDate: Date.UTC(2026, 0, 12), + status: 0, + comments: 0 + } + const milestoneTarget: DragTarget = { kind: 'milestone', doc: milestone } + + it('mousedown-bar on milestone target transitions to dragging-body with kind preserved', () => { + const next = reduce( + { kind: 'idle' }, + { + type: 'mousedown-bar', + target: milestoneTarget, + originStart: milestone.startDate as number, + originEnd: milestone.targetDate as number, + edge: 'body', + cursorX: 200 + }, + ts + ) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.target.kind).toBe('milestone') + if (next.target.kind !== 'milestone') return + expect(next.target.doc._id).toBe(milestone._id) + // Reducer is doc-agnostic — same origin / preview semantics as Issue. + expect(next.originStart).toBe(milestone.startDate) + expect(next.originEnd).toBe(milestone.targetDate) + }) + + it('milestone resize-right preserves target.kind through reducer', () => { + const resizing: DragState = { + kind: 'resizing-right', + target: milestoneTarget, + originStart: milestone.startDate as number, + originEnd: milestone.targetDate as number, + cursorStartX: 250, + previewEnd: milestone.targetDate as number + } + const next = reduce(resizing, { type: 'mousemove', cursorX: 264 }, ts) + if (next.kind !== 'resizing-right') throw new Error('expected resizing-right') + expect(next.target.kind).toBe('milestone') + expect(next.previewEnd).toBe((milestone.targetDate as number) + 1 * 86_400_000) + }) +}) + +describe('drag-controller — bulk co-drag', () => { + // Two members: the leading bar (`issue` above) and a second issue B that + // sits in the bulk-selection. coDrag carries B's origin so the controller + // can clamp the shared delta without GanttView needing to do the math. + const issueB: TestItem = { + _id: 'issue-2', + _class: 'tracker:class:Issue', + space: 'space-1', + startDate: Date.UTC(2026, 0, 19), + dueDate: Date.UTC(2026, 0, 23) + } + + it('mousedown-bar with coDrag transitions to dragging-body carrying the co-drag payload', () => { + const next = reduce( + { kind: 'idle' }, + { + type: 'mousedown-bar', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + edge: 'body', + cursorX: 200, + coDrag: { + members: [ + { + issueId: issue._id, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number + }, + { + issueId: issueB._id, + originStart: issueB.startDate as number, + originEnd: issueB.dueDate as number + } + ], + minDeltaMs: -2 * 86_400_000, + maxDeltaMs: Infinity + } + }, + ts + ) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.coDrag).toBeDefined() + expect(next.coDrag?.members).toHaveLength(2) + expect(next.coDrag?.anchorDeltaMs).toBe(0) + expect(next.coDrag?.minDeltaMs).toBe(-2 * 86_400_000) + }) + + it('mousemove clamps the shared delta to coDrag.minDeltaMs (hard-stop entire group)', () => { + // Week-zoom = 14 px/day. cursorStartX=200, moving to 130 → -5 days. + // coDrag.minDeltaMs caps at -2 days. Both preview AND anchorDeltaMs reflect the clamp. + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: issue.startDate as number, + previewEnd: issue.dueDate as number, + coDrag: { + anchorDeltaMs: 0, + members: [ + { + issueId: issue._id, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number + }, + { + issueId: issueB._id, + originStart: issueB.startDate as number, + originEnd: issueB.dueDate as number + } + ], + minDeltaMs: -2 * 86_400_000, + maxDeltaMs: Infinity + } + } + // 70 px left of cursorStartX → -5 days raw; clamps to -2. + const next = reduce(dragging, { type: 'mousemove', cursorX: 130 }, ts) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.coDrag?.anchorDeltaMs).toBe(-2 * 86_400_000) + expect(next.previewStart).toBe((issue.startDate as number) - 2 * 86_400_000) + expect(next.previewEnd).toBe((issue.dueDate as number) - 2 * 86_400_000) + }) + + it('mousemove still respects ordinary delta when within coDrag bounds', () => { + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: issue.startDate as number, + previewEnd: issue.dueDate as number, + coDrag: { + anchorDeltaMs: 0, + members: [ + { + issueId: issue._id, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number + } + ], + minDeltaMs: -5 * 86_400_000, + maxDeltaMs: Infinity + } + } + // +28 px → +2 days, well within bounds. + const next = reduce(dragging, { type: 'mousemove', cursorX: 228 }, ts) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.coDrag?.anchorDeltaMs).toBe(2 * 86_400_000) + expect(next.previewStart).toBe((issue.startDate as number) + 2 * 86_400_000) + }) + + it('mousemove without coDrag preserves legacy single-bar behaviour (regression)', () => { + const dragging: DragState = { + kind: 'dragging-body', + target: issueTarget, + originStart: issue.startDate as number, + originEnd: issue.dueDate as number, + cursorStartX: 200, + previewStart: issue.startDate as number, + previewEnd: issue.dueDate as number + } + const next = reduce(dragging, { type: 'mousemove', cursorX: 228 }, ts) + if (next.kind !== 'dragging-body') throw new Error('expected dragging-body') + expect(next.coDrag).toBeUndefined() + expect(next.previewStart).toBe((issue.startDate as number) + 2 * 86_400_000) + void snapToUtcMidnight // keep import alive + }) +}) diff --git a/packages/gantt/src/__tests__/drag-state.test.ts b/packages/gantt/src/__tests__/drag-state.test.ts new file mode 100644 index 00000000000..9ed039987d9 --- /dev/null +++ b/packages/gantt/src/__tests__/drag-state.test.ts @@ -0,0 +1,34 @@ +import { activeDragTargetId } from '../drag-state' +import type { DragState, GanttItem } from '../types' + +const issue: GanttItem = { + _id: 'issue-1' +} + +describe('activeDragTargetId', () => { + it('returns the wrapped drag target doc id for edit drags', () => { + const state = { + kind: 'dragging-body', + target: { kind: 'issue', doc: issue }, + originStart: 0, + originEnd: 1, + cursorStartX: 0, + previewStart: 0, + previewEnd: 1 + } satisfies DragState + + expect(activeDragTargetId(state)).toBe('issue-1') + }) + + it('ignores connector target-hover states whose target is a raw issue', () => { + const state = { + kind: 'connector-target-hover', + source: issue, + target: issue, + originPx: { x: 0, y: 0 }, + cursorPx: { x: 10, y: 10 } + } satisfies DragState + + expect(activeDragTargetId(state)).toBeNull() + }) +}) diff --git a/packages/gantt/src/__tests__/flash-store.test.ts b/packages/gantt/src/__tests__/flash-store.test.ts new file mode 100644 index 00000000000..f87b85c0538 --- /dev/null +++ b/packages/gantt/src/__tests__/flash-store.test.ts @@ -0,0 +1,45 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { createFlashStore, flashIssues } from '../flash-store' + +describe('flash-store', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + afterEach(() => { + jest.useRealTimers() + }) + + it('starts empty', () => { + const store = createFlashStore() + expect(store.get().size).toBe(0) + }) + + it('flashIssues adds ids and removes them after durationMs', () => { + const store = createFlashStore() + flashIssues(['a', 'b'], 1000, store) + expect(store.get().has('a')).toBe(true) + expect(store.get().has('b')).toBe(true) + + jest.advanceTimersByTime(999) + expect(store.get().has('a')).toBe(true) + + jest.advanceTimersByTime(2) + expect(store.get().has('a')).toBe(false) + expect(store.get().has('b')).toBe(false) + }) + + it('flashIssues with overlapping ids extends duration to the latest call', () => { + const store = createFlashStore() + flashIssues(['a'], 1000, store) + jest.advanceTimersByTime(500) + flashIssues(['a'], 1000, store) + jest.advanceTimersByTime(800) // total 1300ms since first call but only 800 since second + expect(store.get().has('a')).toBe(true) + jest.advanceTimersByTime(300) + expect(store.get().has('a')).toBe(false) + }) +}) diff --git a/packages/gantt/src/__tests__/gantt-view-options.test.ts b/packages/gantt/src/__tests__/gantt-view-options.test.ts new file mode 100644 index 00000000000..1be2311b1d1 --- /dev/null +++ b/packages/gantt/src/__tests__/gantt-view-options.test.ts @@ -0,0 +1,112 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// +import { + extractGanttSavedView, + mergeGanttSavedView, + isoDateForTimestamp, + timestampForIsoDate, + type GanttSavedViewOptions +} from '../gantt-view-options' + +describe('gantt-view-options', () => { + describe('extractGanttSavedView', () => { + it('returns defaults for empty input', () => { + expect(extractGanttSavedView(undefined)).toEqual({ zoomLevel: 'week' }) + expect(extractGanttSavedView({})).toEqual({ zoomLevel: 'week' }) + }) + + it('reads zoomLevel and panAnchorDate', () => { + expect(extractGanttSavedView({ ganttZoomLevel: 'month', ganttPanAnchorDate: '2026-07-01' })).toEqual({ + zoomLevel: 'month', + panAnchorDate: '2026-07-01' + }) + }) + + it('ignores unknown zoomLevel and falls back to week', () => { + expect(extractGanttSavedView({ ganttZoomLevel: 'century' })).toEqual({ zoomLevel: 'week' }) + }) + + it('ignores malformed panAnchorDate', () => { + expect(extractGanttSavedView({ ganttZoomLevel: 'day', ganttPanAnchorDate: 'not-a-date' })).toEqual({ + zoomLevel: 'day' + }) + }) + + it('ignores non-string zoomLevel', () => { + expect(extractGanttSavedView({ ganttZoomLevel: 42 })).toEqual({ zoomLevel: 'week' }) + }) + }) + + describe('mergeGanttSavedView', () => { + it('preserves unrelated viewOptions keys', () => { + const base = { ganttShowTitle: true, ganttConfirmMove: false } + const out = mergeGanttSavedView(base, { zoomLevel: 'day' }) + expect(out.ganttShowTitle).toBe(true) + expect(out.ganttConfirmMove).toBe(false) + expect(out.ganttZoomLevel).toBe('day') + }) + + it('writes panAnchorDate when present', () => { + const out = mergeGanttSavedView({}, { zoomLevel: 'week', panAnchorDate: '2026-09-15' }) + expect(out.ganttPanAnchorDate).toBe('2026-09-15') + }) + + it('omits panAnchorDate when absent', () => { + const out = mergeGanttSavedView({}, { zoomLevel: 'week' }) + expect(Object.prototype.hasOwnProperty.call(out, 'ganttPanAnchorDate')).toBe(false) + }) + + it('strips stale panAnchorDate from base when new payload has none', () => { + const base = { ganttPanAnchorDate: '2026-01-01', ganttShowTitle: true } + const out = mergeGanttSavedView(base, { zoomLevel: 'week' }) + expect(Object.prototype.hasOwnProperty.call(out, 'ganttPanAnchorDate')).toBe(false) + expect(out.ganttShowTitle).toBe(true) + }) + + it('does not mutate the base object', () => { + const base = { ganttZoomLevel: 'day', ganttShowTitle: true } + const out = mergeGanttSavedView(base, { zoomLevel: 'month' }) + expect(base.ganttZoomLevel).toBe('day') + expect(out.ganttZoomLevel).toBe('month') + }) + + it('accepts undefined base', () => { + const out = mergeGanttSavedView(undefined, { zoomLevel: 'quarter' }) + expect(out.ganttZoomLevel).toBe('quarter') + }) + }) + + describe('iso/timestamp helpers', () => { + it('formats UTC midnight back to YYYY-MM-DD', () => { + const t = Date.UTC(2026, 6, 1) // 2026-07-01 + expect(isoDateForTimestamp(t)).toBe('2026-07-01') + }) + + it('parses YYYY-MM-DD as UTC midnight', () => { + expect(timestampForIsoDate('2026-07-01')).toBe(Date.UTC(2026, 6, 1)) + }) + + it('returns NaN for invalid iso input', () => { + expect(Number.isNaN(timestampForIsoDate('garbage'))).toBe(true) + }) + + it('round-trips arbitrary timestamps via snap-to-midnight', () => { + const t = Date.UTC(2026, 6, 1, 14, 30, 0) + const iso = isoDateForTimestamp(t) + const back = timestampForIsoDate(iso) + expect(back).toBe(Date.UTC(2026, 6, 1)) + }) + + it('pads single-digit month and day', () => { + expect(isoDateForTimestamp(Date.UTC(2026, 0, 5))).toBe('2026-01-05') + }) + }) + + it('round-trips through merge → extract', () => { + const opts: GanttSavedViewOptions = { zoomLevel: 'quarter', panAnchorDate: '2026-03-15' } + const merged = mergeGanttSavedView({ ganttShowTitle: true }, opts) + expect(extractGanttSavedView(merged)).toEqual(opts) + }) +}) diff --git a/packages/gantt/src/__tests__/long-press.test.ts b/packages/gantt/src/__tests__/long-press.test.ts new file mode 100644 index 00000000000..8909598619c --- /dev/null +++ b/packages/gantt/src/__tests__/long-press.test.ts @@ -0,0 +1,111 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { initial, reduceLongPress, LONG_PRESS_MS, MOVE_THRESHOLD_PX, type LongPressState } from '../long-press' + +describe('long-press — initial state', () => { + it('starts idle', () => { + expect(initial()).toEqual({ kind: 'idle' }) + }) +}) + +describe('long-press — start', () => { + it('moves from idle to pending with the start timestamp', () => { + const state = reduceLongPress(initial(), { type: 'start', now: 1000, x: 100, y: 200 }) + expect(state.kind).toBe('pending') + if (state.kind === 'pending') { + expect(state.startedAt).toBe(1000) + expect(state.x).toBe(100) + expect(state.y).toBe(200) + } + }) + + it('ignores a second start while pending', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 1000, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'start', now: 2000, x: 50, y: 50 }) + if (state.kind === 'pending') { + expect(state.startedAt).toBe(1000) // unchanged + } + }) +}) + +describe('long-press — tick threshold', () => { + it('stays pending before threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS - 1 }) + expect(state.kind).toBe('pending') + }) + + it('fires exactly at threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS }) + expect(state.kind).toBe('fired') + }) + + it('fires after threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS + 50 }) + expect(state.kind).toBe('fired') + }) + + it('tick from idle stays idle', () => { + const state = reduceLongPress(initial(), { type: 'tick', now: 1000 }) + expect(state.kind).toBe('idle') + }) + + it('tick when fired stays fired (idempotent)', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS + 1000 }) + expect(state.kind).toBe('fired') + }) +}) + +describe('long-press — move cancellation', () => { + it('cancels when movement exceeds threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 100, y: 200 }) + state = reduceLongPress(state, { type: 'move', now: 50, x: 100 + MOVE_THRESHOLD_PX + 1, y: 200 }) + expect(state.kind).toBe('cancelled') + }) + + it('cancels on diagonal movement past threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 100, y: 200 }) + state = reduceLongPress(state, { type: 'move', now: 50, x: 110, y: 210 }) + // sqrt(10^2 + 10^2) ≈ 14.14, > 10 threshold + expect(state.kind).toBe('cancelled') + }) + + it('stays pending on movement within threshold', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 100, y: 200 }) + state = reduceLongPress(state, { type: 'move', now: 50, x: 105, y: 203 }) + // sqrt(5^2 + 3^2) ≈ 5.83, < 10 threshold + expect(state.kind).toBe('pending') + }) + + it('move from idle stays idle', () => { + const state = reduceLongPress(initial(), { type: 'move', now: 50, x: 9999, y: 9999 }) + expect(state.kind).toBe('idle') + }) + + it('move when fired does not retroactively cancel', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 100, y: 200 }) + state = reduceLongPress(state, { type: 'tick', now: LONG_PRESS_MS }) + state = reduceLongPress(state, { type: 'move', now: LONG_PRESS_MS + 10, x: 9999, y: 9999 }) + expect(state.kind).toBe('fired') + }) +}) + +describe('long-press — explicit cancel', () => { + it('cancel from any active state returns idle', () => { + let state: LongPressState = reduceLongPress(initial(), { type: 'start', now: 0, x: 0, y: 0 }) + state = reduceLongPress(state, { type: 'cancel' }) + expect(state.kind).toBe('idle') + }) + + it('cancel from idle is a no-op', () => { + const state = reduceLongPress(initial(), { type: 'cancel' }) + expect(state.kind).toBe('idle') + }) +}) diff --git a/packages/gantt/src/__tests__/pan-target.test.ts b/packages/gantt/src/__tests__/pan-target.test.ts new file mode 100644 index 00000000000..213a69e56d4 --- /dev/null +++ b/packages/gantt/src/__tests__/pan-target.test.ts @@ -0,0 +1,40 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { shouldPromoteCanvasPan, shouldStartCanvasPan } from '../pan-target' + +function targetWithClosest (matches: Set): Pick { + return { + closest: (selector: string) => { + for (const part of selector.split(',').map((s) => s.trim())) { + if (matches.has(part)) return {} as unknown as Element + } + return null + } + } +} + +describe('pan-target', () => { + it('allows panning from normal gantt bars', () => { + expect(shouldStartCanvasPan(targetWithClosest(new Set(['.bar-wrap'])))).toBe(true) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['rect.bar'])))).toBe(true) + }) + + it('keeps explicit controls out of canvas panning', () => { + expect(shouldStartCanvasPan(targetWithClosest(new Set(['button'])))).toBe(false) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['.resize-cell'])))).toBe(false) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['.drag-grip'])))).toBe(false) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['rect.bar.selected'])))).toBe(false) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['.summary-hit.selected'])))).toBe(false) + expect(shouldStartCanvasPan(targetWithClosest(new Set(['.bar-resize-handle'])))).toBe(false) + }) + + it('promotes click-and-hold to pan only after real pointer movement', () => { + expect(shouldPromoteCanvasPan(0, 0)).toBe(false) + expect(shouldPromoteCanvasPan(2, 3)).toBe(false) + expect(shouldPromoteCanvasPan(4, 0)).toBe(true) + expect(shouldPromoteCanvasPan(0, -4)).toBe(true) + }) +}) diff --git a/packages/gantt/src/__tests__/pinch-zoom.test.ts b/packages/gantt/src/__tests__/pinch-zoom.test.ts new file mode 100644 index 00000000000..d88653c2a83 --- /dev/null +++ b/packages/gantt/src/__tests__/pinch-zoom.test.ts @@ -0,0 +1,141 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + initial, + reducePinch, + computeDistance, + computeCenter, + computePxPerDayFromRatio, + type PinchState +} from '../pinch-zoom' + +describe('pinch-zoom — math helpers', () => { + it('computeDistance: axis-aligned', () => { + expect(computeDistance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5) + }) + + it('computeDistance: zero-vector is 0', () => { + expect(computeDistance({ x: 10, y: 10 }, { x: 10, y: 10 })).toBe(0) + }) + + it('computeCenter: midpoint of two pointers', () => { + expect(computeCenter({ x: 0, y: 0 }, { x: 100, y: 200 })).toEqual({ x: 50, y: 100 }) + }) + + it('computePxPerDayFromRatio: ratio>1 zooms in (more pixels per day)', () => { + expect(computePxPerDayFromRatio(14, 2)).toBe(28) + }) + + it('computePxPerDayFromRatio: ratio<1 zooms out', () => { + expect(computePxPerDayFromRatio(14, 0.5)).toBe(7) + }) + + it('computePxPerDayFromRatio: clamps to MIN/MAX', () => { + expect(computePxPerDayFromRatio(14, 0.0001)).toBeGreaterThan(0) + expect(computePxPerDayFromRatio(14, 10000)).toBeLessThanOrEqual(200) + }) + + it('computePxPerDayFromRatio: handles non-finite ratio', () => { + expect(computePxPerDayFromRatio(14, Number.NaN)).toBe(14) + expect(computePxPerDayFromRatio(14, 0)).toBe(14) + }) +}) + +describe('pinch-zoom — single-pointer tracking', () => { + it('starts idle', () => { + expect(initial().kind).toBe('idle') + }) + + it('down → single', () => { + const s = reducePinch(initial(), { type: 'down', id: 1, x: 50, y: 50, pxPerDay: 14 }) + expect(s.kind).toBe('single') + }) + + it('move with single-pointer does not zoom', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 50, y: 50, pxPerDay: 14 }) + s = reducePinch(s, { type: 'move', id: 1, x: 100, y: 80 }) + expect(s.kind).toBe('single') + }) + + it('up from single returns to idle', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 50, y: 50, pxPerDay: 14 }) + s = reducePinch(s, { type: 'up', id: 1 }) + expect(s.kind).toBe('idle') + }) +}) + +describe('pinch-zoom — two-pointer pinch', () => { + it('second down → pinch with initial distance + center captured', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + expect(s.kind).toBe('pinch') + if (s.kind === 'pinch') { + expect(s.initialDistance).toBe(100) + expect(s.center).toEqual({ x: 50, y: 0 }) + expect(s.initialPxPerDay).toBe(14) + } + }) + + it('move during pinch updates current ratio', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + // Spread fingers apart: id=2 now at x=200 → new distance = 200, ratio = 2 + s = reducePinch(s, { type: 'move', id: 2, x: 200, y: 0 }) + expect(s.kind).toBe('pinch') + if (s.kind === 'pinch') { + expect(s.currentDistance).toBe(200) + } + }) + + it('pinching closer reduces distance', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'move', id: 2, x: 50, y: 0 }) + if (s.kind === 'pinch') { + expect(s.currentDistance).toBe(50) + } + }) + + it('lifting one finger drops back to single (keeps tracking the other)', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'up', id: 2 }) + expect(s.kind).toBe('single') + }) + + it('cancel always returns to idle (iOS pointercancel during scroll)', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'cancel' }) + expect(s.kind).toBe('idle') + }) + + it('center recomputes from the original captured midpoint, not the live one', () => { + // The zoom anchor is the midpoint between the two fingers AT PINCH-START. + // We hold the captured center stable so the zoom doesn't drift if the + // fingers slide together. + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'move', id: 2, x: 200, y: 0 }) + if (s.kind === 'pinch') { + expect(s.center).toEqual({ x: 50, y: 0 }) // captured at down#2 + } + }) + + it('ignores moves from untracked pointer ids', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'move', id: 999, x: 50, y: 50 }) + expect(s.kind).toBe('single') + }) + + it('down for a third pointer keeps the pinch (only first 2 are tracked)', () => { + let s: PinchState = reducePinch(initial(), { type: 'down', id: 1, x: 0, y: 0, pxPerDay: 14 }) + s = reducePinch(s, { type: 'down', id: 2, x: 100, y: 0, pxPerDay: 14 }) + const before = s + s = reducePinch(s, { type: 'down', id: 3, x: 999, y: 999, pxPerDay: 14 }) + expect(s).toEqual(before) + }) +}) diff --git a/packages/gantt/src/__tests__/pointer-classify.test.ts b/packages/gantt/src/__tests__/pointer-classify.test.ts new file mode 100644 index 00000000000..d4d5b0ce3b0 --- /dev/null +++ b/packages/gantt/src/__tests__/pointer-classify.test.ts @@ -0,0 +1,61 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { classifyPointer, type PointerAction } from '../pointer-classify' + +describe('pointer-classify — phone (read-only)', () => { + it('allows tap on touch', () => { + expect(classifyPointer('phone', 'touch', 'tap')).toBe('allow') + }) + it('blocks drag on touch', () => { + expect(classifyPointer('phone', 'touch', 'drag')).toBe('block') + }) + it('blocks resize on touch', () => { + expect(classifyPointer('phone', 'touch', 'resize')).toBe('block') + }) + it('blocks connector on touch', () => { + expect(classifyPointer('phone', 'touch', 'connector')).toBe('block') + }) + it('blocks mouse drag too — phone is strictly read-only regardless of input', () => { + expect(classifyPointer('phone', 'mouse', 'drag')).toBe('block') + }) + it('treats pen identically to touch on phone', () => { + expect(classifyPointer('phone', 'pen', 'drag')).toBe('block') + expect(classifyPointer('phone', 'pen', 'tap')).toBe('allow') + }) +}) + +describe('pointer-classify — tablet (full edit, touch needs long-press)', () => { + it('requires long-press for touch drag', () => { + expect(classifyPointer('tablet', 'touch', 'drag')).toBe('long-press') + }) + it('requires long-press for touch resize', () => { + expect(classifyPointer('tablet', 'touch', 'resize')).toBe('long-press') + }) + it('requires long-press for touch connector', () => { + expect(classifyPointer('tablet', 'touch', 'connector')).toBe('long-press') + }) + it('allows direct drag for mouse', () => { + expect(classifyPointer('tablet', 'mouse', 'drag')).toBe('allow') + }) + it('allows tap on touch directly', () => { + expect(classifyPointer('tablet', 'touch', 'tap')).toBe('allow') + }) + it('treats pen as mouse-equivalent (Apple Pencil with hardware keyboard)', () => { + expect(classifyPointer('tablet', 'pen', 'drag')).toBe('allow') + }) +}) + +describe('pointer-classify — desktop', () => { + it('allows everything directly for mouse', () => { + const actions: PointerAction[] = ['tap', 'drag', 'resize', 'connector'] + for (const a of actions) { + expect(classifyPointer('desktop', 'mouse', a)).toBe('allow') + } + }) + it('still long-presses touch on desktop (touch-laptop case)', () => { + expect(classifyPointer('desktop', 'touch', 'drag')).toBe('long-press') + }) +}) diff --git a/packages/gantt/src/__tests__/sidebar-columns.test.ts b/packages/gantt/src/__tests__/sidebar-columns.test.ts new file mode 100644 index 00000000000..e4af36e340c --- /dev/null +++ b/packages/gantt/src/__tests__/sidebar-columns.test.ts @@ -0,0 +1,168 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + parseColumns, + parseWidths, + clampWidth, + computeTotalWidth, + DEFAULT_WIDTHS, + DEFAULT_COLUMNS, + ALL_COLUMN_KEYS, + MIN_WIDTH, + MAX_WIDTH, + type SidebarColumnKey +} from '../sidebar-columns' + +describe('sidebar-columns: parseColumns', () => { + it('returns DEFAULT_COLUMNS for undefined', () => { + expect(parseColumns(undefined)).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS for null', () => { + expect(parseColumns(null)).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS for non-array values', () => { + expect(parseColumns(42)).toEqual(DEFAULT_COLUMNS) + expect(parseColumns('identifier')).toEqual(DEFAULT_COLUMNS) + expect(parseColumns({ identifier: true })).toEqual(DEFAULT_COLUMNS) + }) + + it('keeps a valid single-column array', () => { + expect(parseColumns(['identifier'])).toEqual(['identifier']) + }) + + it('filters out unknown column keys', () => { + expect(parseColumns(['identifier', 'bogus', 'title'])).toEqual(['identifier', 'title']) + }) + + it('returns DEFAULT_COLUMNS when array is empty (min 1 column rule)', () => { + expect(parseColumns([])).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS when all entries are invalid', () => { + expect(parseColumns(['bogus', 'other'])).toEqual(DEFAULT_COLUMNS) + }) + + it('preserves order of valid entries', () => { + expect(parseColumns(['title', 'identifier'])).toEqual(['title', 'identifier']) + }) + + it('deduplicates repeated keys', () => { + expect(parseColumns(['title', 'title', 'identifier'])).toEqual(['title', 'identifier']) + }) +}) + +describe('sidebar-columns: clampWidth', () => { + it('clamps below MIN_WIDTH', () => { + expect(clampWidth(20)).toBe(MIN_WIDTH) + expect(clampWidth(0)).toBe(MIN_WIDTH) + expect(clampWidth(-5)).toBe(MIN_WIDTH) + }) + + it('clamps above MAX_WIDTH', () => { + expect(clampWidth(MAX_WIDTH + 100)).toBe(MAX_WIDTH) + expect(clampWidth(10_000)).toBe(MAX_WIDTH) + }) + + it('returns value unchanged within range', () => { + expect(clampWidth(40)).toBe(40) + expect(clampWidth(150)).toBe(150) + expect(clampWidth(400)).toBe(400) + }) + + it('rounds fractional pixel inputs', () => { + expect(clampWidth(150.7)).toBe(151) + expect(clampWidth(150.3)).toBe(150) + }) +}) + +describe('sidebar-columns: constants', () => { + it('DEFAULT_WIDTHS covers every ALL_COLUMN_KEYS entry', () => { + for (const key of ALL_COLUMN_KEYS) { + expect(typeof DEFAULT_WIDTHS[key]).toBe('number') + expect(DEFAULT_WIDTHS[key]).toBeGreaterThanOrEqual(MIN_WIDTH) + expect(DEFAULT_WIDTHS[key]).toBeLessThanOrEqual(MAX_WIDTH) + } + }) + + it('DEFAULT_COLUMNS is a subset of ALL_COLUMN_KEYS', () => { + for (const k of DEFAULT_COLUMNS) { + expect(ALL_COLUMN_KEYS).toContain(k as SidebarColumnKey) + } + }) + + it('MIN_WIDTH < MAX_WIDTH', () => { + expect(MIN_WIDTH).toBeLessThan(MAX_WIDTH) + }) +}) + +describe('sidebar-columns: computeTotalWidth', () => { + it('sums explicit widths for every visible column', () => { + const cols: SidebarColumnKey[] = ['identifier', 'title', 'predecessors', 'slack'] + const widths = { identifier: 80, title: 240, predecessors: 140, slack: 60 } + expect(computeTotalWidth(cols, widths)).toBe(520) + }) + + it('falls back to DEFAULT_WIDTHS when a column has no override', () => { + const cols: SidebarColumnKey[] = ['identifier', 'title'] + expect(computeTotalWidth(cols, {})).toBe(DEFAULT_WIDTHS.identifier + DEFAULT_WIDTHS.title) + }) + + it('matches DEFAULT_COLUMNS sum when widths is empty', () => { + const expected = DEFAULT_COLUMNS.reduce((s, c) => s + DEFAULT_WIDTHS[c], 0) + expect(computeTotalWidth(DEFAULT_COLUMNS, {})).toBe(expected) + }) + + it('returns 0 for empty column list', () => { + expect(computeTotalWidth([], { identifier: 80 })).toBe(0) + }) + + it('rejects negative width overrides and uses the default instead', () => { + const cols: SidebarColumnKey[] = ['identifier'] + expect(computeTotalWidth(cols, { identifier: -5 })).toBe(DEFAULT_WIDTHS.identifier) + }) + + it('coerces non-finite override values (NaN, Infinity) to the default', () => { + const cols: SidebarColumnKey[] = ['identifier'] + expect(computeTotalWidth(cols, { identifier: NaN })).toBe(DEFAULT_WIDTHS.identifier) + expect(computeTotalWidth(cols, { identifier: Infinity })).toBe(DEFAULT_WIDTHS.identifier) + }) +}) + +describe('sidebar-columns: parseWidths', () => { + it('returns the defaults for undefined / null / non-objects / arrays', () => { + expect(parseWidths(undefined)).toEqual(DEFAULT_WIDTHS) + expect(parseWidths(null)).toEqual(DEFAULT_WIDTHS) + expect(parseWidths('slack')).toEqual(DEFAULT_WIDTHS) + expect(parseWidths([['slack', 200]])).toEqual(DEFAULT_WIDTHS) + }) + + it('overrides only the supplied columns and keeps defaults for the rest', () => { + const out = parseWidths({ slack: 120, title: 300 }) + expect(out.slack).toBe(120) + expect(out.title).toBe(300) + expect(out.identifier).toBe(DEFAULT_WIDTHS.identifier) + expect(Object.keys(out).sort()).toEqual([...ALL_COLUMN_KEYS].sort()) + }) + + it('drops unknown keys and non-numeric values', () => { + const out = parseWidths({ nope: 500, slack: 'wide', predecessors: Number.NaN }) + expect(out).toEqual(DEFAULT_WIDTHS) + }) + + it('clamps out-of-range widths', () => { + const out = parseWidths({ slack: -50, title: 100_000 }) + expect(out.slack).toBe(MIN_WIDTH) + expect(out.title).toBe(MAX_WIDTH) + }) + + it('round-trips a persisted width through computeTotalWidth', () => { + const cols: SidebarColumnKey[] = ['identifier', 'slack'] + const widths = parseWidths({ slack: 111 }) + expect(computeTotalWidth(cols, widths)).toBe(DEFAULT_WIDTHS.identifier + 111) + }) +}) diff --git a/packages/gantt/src/__tests__/time-scale.test.ts b/packages/gantt/src/__tests__/time-scale.test.ts new file mode 100644 index 00000000000..bc3393f5e34 --- /dev/null +++ b/packages/gantt/src/__tests__/time-scale.test.ts @@ -0,0 +1,143 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { createTimeScale, snapToUtcMidnight, toGanttDay } from '../time-scale' + +const DAY_MS = 86_400_000 + +describe('toGanttDay', () => { + it('maps null and undefined to null', () => { + expect(toGanttDay(null)).toBeNull() + expect(toGanttDay(undefined)).toBeNull() + }) + + it('snaps a mid-day timestamp to its UTC midnight', () => { + const t = Date.UTC(2026, 4, 15, 17, 30, 45) + expect(toGanttDay(t)).toBe(Date.UTC(2026, 4, 15)) + }) + + it('collapses any time-of-day on the same UTC calendar day to one value (no drag-jump)', () => { + const morning = Date.UTC(2026, 4, 15, 1, 0, 0) + const evening = Date.UTC(2026, 4, 15, 23, 59, 0) + expect(toGanttDay(morning)).toBe(toGanttDay(evening)) + expect(toGanttDay(morning)).toBe(Date.UTC(2026, 4, 15)) + }) + + it('is idempotent (already-snapped value is unchanged)', () => { + const snapped = Date.UTC(2026, 0, 1) + expect(toGanttDay(snapped)).toBe(snapped) + }) +}) + +describe('snapToUtcMidnight', () => { + it('returns 0 unchanged', () => { + expect(snapToUtcMidnight(0)).toBe(0) + }) + + it('rounds down to UTC midnight', () => { + const t = Date.UTC(2026, 4, 15, 17, 30, 45) // 2026-05-15 17:30:45 UTC + expect(snapToUtcMidnight(t)).toBe(Date.UTC(2026, 4, 15)) + }) + + it('is idempotent', () => { + const t = Date.UTC(2026, 0, 1) + expect(snapToUtcMidnight(snapToUtcMidnight(t))).toBe(t) + }) +}) + +describe('createTimeScale', () => { + const origin = Date.UTC(2026, 0, 1) // 2026-01-01 UTC + + it('week zoom: pxPerDay = 14', () => { + const ts = createTimeScale('week', origin) + expect(ts.pxPerDay).toBe(14) + }) + + it('day/month/quarter zoom values match preset', () => { + expect(createTimeScale('day', origin).pxPerDay).toBe(32) + expect(createTimeScale('month', origin).pxPerDay).toBe(4) + expect(createTimeScale('quarter', origin).pxPerDay).toBe(1.5) + }) + + it('supports an adaptive pxPerDay override', () => { + const ts = createTimeScale('quarter', origin, 2.25) + expect(ts.pxPerDay).toBe(2.25) + expect(ts.toX(origin + 10 * DAY_MS)).toBe(22.5) + }) + + it('toX(origin) === 0', () => { + const ts = createTimeScale('week', origin) + expect(ts.toX(origin)).toBe(0) + }) + + it('toX(origin + 7d) === 7 * pxPerDay', () => { + const ts = createTimeScale('week', origin) + expect(ts.toX(origin + 7 * DAY_MS)).toBe(7 * 14) + }) + + it('fromX is inverse of toX (snapped)', () => { + const ts = createTimeScale('week', origin) + const t = origin + 5 * DAY_MS + expect(ts.fromX(ts.toX(t))).toBe(t) + }) + + it('week zoom emits weekly ticks aligned to Monday', () => { + const ts = createTimeScale('week', origin) + const ticks = ts.ticks([origin, origin + 30 * DAY_MS]) + expect(ticks.length).toBeGreaterThanOrEqual(4) + expect(ticks.length).toBeLessThanOrEqual(6) + // First tick is the first Monday on or after origin + expect(ticks[0].date).toBeGreaterThanOrEqual(origin) + expect(ticks[0].date).toBeLessThan(origin + 7 * DAY_MS) + // Every tick is on a Monday. + for (const t of ticks) { + expect(new Date(t.date).getUTCDay()).toBe(1) + } + expect(ticks.every((t) => Number.isInteger(t.date))).toBe(true) + }) + + it('all tick dates are UTC midnights', () => { + const ts = createTimeScale('week', origin) + const ticks = ts.ticks([origin, origin + 30 * DAY_MS]) + for (const t of ticks) { + expect(snapToUtcMidnight(t.date)).toBe(t.date) + } + }) +}) + +describe('time-scale — secondary label (year/month supra row)', () => { + const DAY = 86_400_000 + + it('day-zoom: secondaryLabel set on 1st of month and on first visible tick', () => { + const scale = createTimeScale('day', Date.UTC(2026, 0, 1)) + // Span 2 months: Jan 28 – Feb 5 + const ticks = scale.ticks([Date.UTC(2026, 0, 28), Date.UTC(2026, 1, 5)]) + const withSecondary = ticks.filter((t) => t.secondaryLabel !== undefined) + // Expect: first visible tick (Jan 28) + Feb 1 + expect(withSecondary).toHaveLength(2) + expect(withSecondary[0].secondaryLabel).toMatch(/jan/i) + expect(new Date(withSecondary[1].date).getUTCDate()).toBe(1) + expect(withSecondary[1].secondaryLabel).toMatch(/feb/i) + }) + + it('week-zoom: secondaryLabel = year on first visible week + first week of new year', () => { + const scale = createTimeScale('week', Date.UTC(2026, 11, 1)) + // Span across the 2026/2027 year boundary + const ticks = scale.ticks([Date.UTC(2026, 11, 1), Date.UTC(2027, 0, 31)]) + const withSecondary = ticks.filter((t) => t.secondaryLabel !== undefined) + expect(withSecondary.length).toBeGreaterThanOrEqual(2) + expect(withSecondary[0].secondaryLabel).toBe('2026') + expect(withSecondary[1].secondaryLabel).toBe('2027') + }) + + it('quarter-zoom: label is "Qn" only, year goes to secondaryLabel', () => { + const scale = createTimeScale('quarter', Date.UTC(2026, 0, 1)) + const ticks = scale.ticks([Date.UTC(2026, 0, 1), Date.UTC(2026, 11, 31)]) + expect(ticks.map((t) => t.label)).toEqual(['Q1', 'Q2', 'Q3', 'Q4']) + expect(ticks[0].secondaryLabel).toBe('2026') + expect(ticks[1].secondaryLabel).toBeUndefined() // Q2 same year + void DAY + }) +}) diff --git a/packages/gantt/src/__tests__/toolbar-overflow.test.ts b/packages/gantt/src/__tests__/toolbar-overflow.test.ts new file mode 100644 index 00000000000..59939b57e1e --- /dev/null +++ b/packages/gantt/src/__tests__/toolbar-overflow.test.ts @@ -0,0 +1,131 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + TIER_FALLBACK_PX, + TOOLBAR_COLLAPSE_ORDER, + TOOLBAR_TIERS, + computeToolbarOverflow, + type ToolbarTier +} from '../toolbar-overflow' + +const ALL: ToolbarTier[] = [...TOOLBAR_TIERS] + +// One easy-to-reason-about width per tier. +const W: Record = { + group: 100, + nav: 200, + date: 150, + zoom: 120, + undo: 60, + savedview: 90 +} + +const GAP = 4 +const MORE = 28 + +describe('computeToolbarOverflow', () => { + it('keeps everything inline when the available width is not measured yet', () => { + expect(computeToolbarOverflow(ALL, W, -1, GAP, MORE)).toEqual({ visible: ALL, hidden: [] }) + expect(computeToolbarOverflow(ALL, W, Number.NaN, GAP, MORE)).toEqual({ visible: ALL, hidden: [] }) + }) + + it('treats a measured zero width as "collapse everything", not as unmeasured', () => { + // A cluster squeezed to 0 px must not re-expand into a box it cannot fit + // into — that is what pushed the trailing buttons out of the viewport. + expect(computeToolbarOverflow(ALL, W, 0, GAP, MORE)).toEqual({ visible: [], hidden: ALL }) + }) + + it('keeps everything inline when it fits exactly', () => { + // 100+200+150+120+60+90 = 720, plus five gaps = 740 + const r = computeToolbarOverflow(ALL, W, 740, GAP, MORE) + expect(r.hidden).toEqual([]) + expect(r.visible).toEqual(ALL) + }) + + it('collapses the least important tier first', () => { + const r = computeToolbarOverflow(ALL, W, 739, GAP, MORE) + expect(r.hidden).toEqual(['savedview']) + expect(r.visible).toEqual(['group', 'nav', 'date', 'zoom', 'undo']) + }) + + it('collapses further tiers in TOOLBAR_COLLAPSE_ORDER as space shrinks', () => { + expect(computeToolbarOverflow(ALL, W, 600, GAP, MORE).hidden).toEqual(['date', 'savedview']) + expect(computeToolbarOverflow(ALL, W, 450, GAP, MORE).hidden).toEqual(['group', 'date', 'savedview']) + expect(computeToolbarOverflow(ALL, W, 400, GAP, MORE).hidden).toEqual(['group', 'date', 'undo', 'savedview']) + expect(computeToolbarOverflow(ALL, W, 300, GAP, MORE).hidden).toEqual([ + 'group', + 'date', + 'zoom', + 'undo', + 'savedview' + ]) + }) + + it('collapses everything on a phone-sized budget', () => { + const r = computeToolbarOverflow(ALL, W, 40, GAP, MORE) + expect(r.visible).toEqual([]) + expect(r.hidden).toEqual(ALL) + }) + + it('never drops a tier — visible + hidden is always the input set', () => { + for (const available of [-1, 0, 30, 120, 300, 500, 740, 2000]) { + const r = computeToolbarOverflow(ALL, W, available, GAP, MORE) + expect([...r.visible, ...r.hidden].sort()).toEqual([...ALL].sort()) + } + }) + + it('preserves the caller-supplied visual order in both buckets', () => { + const r = computeToolbarOverflow(ALL, W, 300, GAP, MORE) + expect(r.visible).toEqual(ALL.filter((t) => !r.hidden.includes(t))) + expect(r.hidden).toEqual(ALL.filter((t) => r.hidden.includes(t))) + }) + + it('ignores tiers that are not present', () => { + const present: ToolbarTier[] = ['nav', 'zoom'] + // nav(200) + zoom(120) + gap = 324 > 240; nav alone plus the "…" trigger + // is 200 + 4 + 28 = 232 and fits. + const r = computeToolbarOverflow(present, W, 240, GAP, MORE) + expect(r.visible).toEqual(['nav']) + expect(r.hidden).toEqual(['zoom']) + }) + + it('falls back to TIER_FALLBACK_PX for unmeasured tiers', () => { + const present: ToolbarTier[] = ['nav', 'zoom'] + // Both unmeasured: 2 * fallback + one gap. + const fits = 2 * TIER_FALLBACK_PX + GAP + expect(computeToolbarOverflow(present, {}, fits, GAP, MORE).hidden).toEqual([]) + expect(computeToolbarOverflow(present, {}, fits - 1, GAP, MORE).hidden).toEqual(['zoom']) + }) + + it('accounts for the "…" trigger once collapsing has started', () => { + const present: ToolbarTier[] = ['nav', 'savedview'] + // nav(200) alone would fit into 210, but the trigger adds 28 + 4 gap. + const r = computeToolbarOverflow(present, W, 210, GAP, MORE) + expect(r.visible).toEqual([]) + expect(r.hidden).toEqual(present) + }) + + it('reserves nothing for a trigger rendered outside the measured box', () => { + // What production passes: the "…" trigger lives in the frozen trailing + // group (it would be clipped away inside a cluster that legitimately + // shrinks to 0 px), so neither its width nor a gap for it may be charged + // to the cluster's budget. + const present: ToolbarTier[] = ['nav', 'savedview'] + const r = computeToolbarOverflow(present, W, 210, GAP, 0) + expect(r.visible).toEqual(['nav']) + expect(r.hidden).toEqual(['savedview']) + }) + + it('exposes a collapse order that covers every tier exactly once', () => { + expect([...TOOLBAR_COLLAPSE_ORDER].sort()).toEqual([...TOOLBAR_TIERS].sort()) + }) + + it('is idempotent for a stable measurement', () => { + const first = computeToolbarOverflow(ALL, W, 500, GAP, MORE) + const second = computeToolbarOverflow(ALL, W, 500, GAP, MORE) + expect(second).toEqual(first) + }) +}) diff --git a/packages/gantt/src/__tests__/tree-expand-store.test.ts b/packages/gantt/src/__tests__/tree-expand-store.test.ts new file mode 100644 index 00000000000..e37475af911 --- /dev/null +++ b/packages/gantt/src/__tests__/tree-expand-store.test.ts @@ -0,0 +1,116 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { createTreeExpandStore } from '../tree-expand-store' + +function makeStorage (): Storage { + const map = new Map() + return { + get length (): number { + return map.size + }, + clear: () => { + map.clear() + }, + getItem: (k: string) => map.get(k) ?? null, + key: (i: number) => Array.from(map.keys())[i] ?? null, + removeItem: (k: string) => { + map.delete(k) + }, + setItem: (k: string, v: string) => { + map.set(k, v) + } + } +} + +describe('createTreeExpandStore', () => { + it('returns an empty set when nothing is persisted', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + expect(s.getCollapsed().size).toBe(0) + }) + + it('persists toggle across re-creation for the same project', () => { + const storage = makeStorage() + const s1 = createTreeExpandStore('proj-A', storage) + s1.toggle('issue:foo') + expect(s1.getCollapsed().has('issue:foo')).toBe(true) + const s2 = createTreeExpandStore('proj-A', storage) + expect(s2.getCollapsed().has('issue:foo')).toBe(true) + }) + + it('toggles an existing entry off', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + s.toggle('issue:foo') + s.toggle('issue:foo') + expect(s.getCollapsed().size).toBe(0) + }) + + it('isolates state per project key', () => { + const storage = makeStorage() + const a = createTreeExpandStore('proj-A', storage) + const b = createTreeExpandStore('proj-B', storage) + a.toggle('issue:x') + expect(b.getCollapsed().has('issue:x')).toBe(false) + }) + + it('setAll replaces the entire state', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + s.toggle('issue:1') + s.setAll(new Set(['issue:2', 'issue:3'])) + expect(s.getCollapsed().has('issue:1')).toBe(false) + expect(s.getCollapsed().has('issue:2')).toBe(true) + expect(s.getCollapsed().has('issue:3')).toBe(true) + }) + + it('expandAll clears all collapsed ids', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + s.setAll(new Set(['issue:1', 'issue:2'])) + s.expandAll() + expect(s.getCollapsed().size).toBe(0) + }) + + it('collapseAll fills state with the given ids', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + s.collapseAll(['issue:1', 'issue:2']) + expect(s.getCollapsed().size).toBe(2) + }) + + it('survives a corrupt persisted value', () => { + const storage = makeStorage() + storage.setItem('huly:gantt:tree-collapsed:proj-A', 'not-json') + const s = createTreeExpandStore('proj-A', storage) + expect(s.getCollapsed().size).toBe(0) + }) + + it('rejects non-array persisted JSON', () => { + const storage = makeStorage() + storage.setItem('huly:gantt:tree-collapsed:proj-A', '{"foo":true}') + const s = createTreeExpandStore('proj-A', storage) + expect(s.getCollapsed().size).toBe(0) + }) + + it('subscribe fires with current snapshot then on each change', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + const sizes: number[] = [] + s.subscribe((set) => { + sizes.push(set.size) + }) + s.toggle('issue:1') + s.toggle('issue:2') + s.expandAll() + expect(sizes).toEqual([0, 1, 2, 0]) + }) + + it('unsubscribe stops notifications', () => { + const s = createTreeExpandStore('proj-A', makeStorage()) + const sizes: number[] = [] + const unsub = s.subscribe((set) => { + sizes.push(set.size) + }) + unsub() + s.toggle('issue:1') + expect(sizes).toEqual([0]) + }) +}) diff --git a/packages/gantt/src/__tests__/types.test.ts b/packages/gantt/src/__tests__/types.test.ts new file mode 100644 index 00000000000..5af467335e0 --- /dev/null +++ b/packages/gantt/src/__tests__/types.test.ts @@ -0,0 +1,25 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { DragTarget, GanttDependency, GanttItem, WorkingCalendar } from '../types' + +it('accepts a structural tracker-like target without importing tracker', () => { + const item: GanttItem = { _id: 'ISSUE-1' } + const target: DragTarget = { kind: 'issue', doc: item } + expect(target.doc._id).toBe('ISSUE-1') + const cal: WorkingCalendar = { weekdayMask: 31, holidays: [] } + expect(cal.weekdayMask).toBe(31) +}) + +it('models a neutral scheduling dependency', () => { + const dep: GanttDependency = { _id: 'REL-1', fromId: 'ISSUE-1', toId: 'ISSUE-2', kind: 'FS' } + expect(dep.fromId).toBe('ISSUE-1') + expect(dep.toId).toBe('ISSUE-2') + expect(dep.kind).toBe('FS') + expect(dep.lagDays).toBeUndefined() + + const withLag: GanttDependency = { ...dep, kind: 'SS', lagDays: 3 } + expect(withLag.lagDays).toBe(3) +}) diff --git a/packages/gantt/src/__tests__/viewport.test.ts b/packages/gantt/src/__tests__/viewport.test.ts new file mode 100644 index 00000000000..6587f874617 --- /dev/null +++ b/packages/gantt/src/__tests__/viewport.test.ts @@ -0,0 +1,68 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + computeAdaptivePxPerDay, + computeCanvasRenderWidth, + computeCanvasViewportWidth, + computeTickViewport, + nonWorkingDaysInRange +} from '../viewport' +import type { WorkingCalendar } from '../types' + +describe('viewport', () => { + it('subtracts the sticky sidebar and resize cell from the visible canvas width', () => { + expect(computeCanvasViewportWidth(873, 280, 5)).toBe(588) + expect(computeCanvasViewportWidth(433, 280, 5)).toBe(148) + }) + + it('never returns zero or negative width', () => { + expect(computeCanvasViewportWidth(100, 280, 5)).toBe(1) + }) + + it('fills the visible canvas when the data range is narrower than the viewport', () => { + expect(computeCanvasRenderWidth(480, 900)).toBe(900) + expect(computeCanvasRenderWidth(1400, 900)).toBe(1400) + }) + + it('clamps header and grid tick generation to the data range', () => { + expect(computeTickViewport(0, 1600, 900)).toEqual({ left: 0, right: 900 }) + expect(computeTickViewport(500, 1600, 900)).toEqual({ left: 400, right: 900 }) + }) + + it('stretches time columns when the data range is narrower than the viewport', () => { + expect(computeAdaptivePxPerDay(1.5, 600, 900)).toBeCloseTo(2.25) + expect(computeAdaptivePxPerDay(1.5, 1200, 900)).toBe(1.5) + }) +}) + +describe('nonWorkingDaysInRange', () => { + const cfgMonFri: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [] } + + it('returns an empty array when cfg is undefined (legacy mode)', () => { + expect(nonWorkingDaysInRange(Date.UTC(2026, 4, 18), Date.UTC(2026, 4, 25), undefined)).toEqual([]) + }) + + it('lists Saturday and Sunday for a Mon..next-Mon range under Mon-Fri', () => { + const res = nonWorkingDaysInRange(Date.UTC(2026, 4, 18), Date.UTC(2026, 4, 25), cfgMonFri) + expect(res).toEqual([Date.UTC(2026, 4, 23), Date.UTC(2026, 4, 24)]) + }) + + it('respects the maxDays cap', () => { + const res = nonWorkingDaysInRange(Date.UTC(2026, 0, 1), Date.UTC(2030, 0, 1), cfgMonFri, 10) + expect(res.length).toBeLessThanOrEqual(10) + }) + + it('handles inverted ranges (from > to) by normalising', () => { + const res = nonWorkingDaysInRange(Date.UTC(2026, 4, 25), Date.UTC(2026, 4, 18), cfgMonFri) + expect(res).toEqual([Date.UTC(2026, 4, 23), Date.UTC(2026, 4, 24)]) + }) + + it('includes a holiday as a non-working day', () => { + const cfg: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [Date.UTC(2026, 4, 20)] } + const res = nonWorkingDaysInRange(Date.UTC(2026, 4, 18), Date.UTC(2026, 4, 22), cfg) + expect(res).toEqual([Date.UTC(2026, 4, 20)]) + }) +}) diff --git a/packages/gantt/src/__tests__/working-days.test.ts b/packages/gantt/src/__tests__/working-days.test.ts new file mode 100644 index 00000000000..e4821171b37 --- /dev/null +++ b/packages/gantt/src/__tests__/working-days.test.ts @@ -0,0 +1,243 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + isWorkingDay, + nextWorkingDay, + addWorkingDays, + workingDaysBetween, + fsAnchor, + ssAnchor, + ffAnchor, + sfAnchor, + fsReverseAnchor, + ssReverseAnchor, + ffReverseAnchor, + sfReverseAnchor +} from '../working-days' +import type { WorkingCalendar } from '../types' + +const DAY_MS = 86_400_000 + +// Calendar anchors used across the suite — UTC 2026-05 week: +// Mon May 18 .. Mon May 25 +const MON = Date.UTC(2026, 4, 18) +const TUE = Date.UTC(2026, 4, 19) +const WED = Date.UTC(2026, 4, 20) +const FRI = Date.UTC(2026, 4, 22) +const SAT = Date.UTC(2026, 4, 23) +const SUN = Date.UTC(2026, 4, 24) +const MON2 = Date.UTC(2026, 4, 25) + +const cfgMonFri: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [] } +const cfgAllDays: WorkingCalendar = { weekdayMask: 0b1111111, holidays: [] } +const cfgEmpty: WorkingCalendar = { weekdayMask: 0, holidays: [] } + +describe('isWorkingDay', () => { + it('Monday is a working day under Mon-Fri', () => { + expect(isWorkingDay(MON, cfgMonFri)).toBe(true) + }) + + it('Friday is a working day under Mon-Fri', () => { + expect(isWorkingDay(FRI, cfgMonFri)).toBe(true) + }) + + it('Saturday is not a working day under Mon-Fri', () => { + expect(isWorkingDay(SAT, cfgMonFri)).toBe(false) + }) + + it('Sunday is not a working day under Mon-Fri', () => { + expect(isWorkingDay(SUN, cfgMonFri)).toBe(false) + }) + + it('Saturday is a working day when bit 5 is set', () => { + expect(isWorkingDay(SAT, { weekdayMask: 0b0111111, holidays: [] })).toBe(true) + }) + + it('Sunday is a working day when bit 6 is set', () => { + expect(isWorkingDay(SUN, { weekdayMask: 0b1111111, holidays: [] })).toBe(true) + }) + + it('respects holiday entries', () => { + expect(isWorkingDay(MON, { weekdayMask: 0b0011111, holidays: [MON] })).toBe(false) + }) + + it('rounds the input to UTC midnight before evaluating', () => { + // Half a day past Monday is still Monday. + expect(isWorkingDay(MON + DAY_MS / 2, cfgMonFri)).toBe(true) + }) + + it('matches a holiday entry that is not midnight-aligned', () => { + expect(isWorkingDay(MON, { weekdayMask: 0b0011111, holidays: [MON + 12 * 3600_000] })).toBe(false) + }) + + it('an empty mask treats every day as non-working', () => { + expect(isWorkingDay(MON, cfgEmpty)).toBe(false) + expect(isWorkingDay(WED, cfgEmpty)).toBe(false) + }) +}) + +describe('nextWorkingDay', () => { + it('returns the input if it is already a working day', () => { + expect(nextWorkingDay(MON, cfgMonFri)).toBe(MON) + }) + + it('skips Saturday → Monday', () => { + expect(nextWorkingDay(SAT, cfgMonFri)).toBe(MON2) + }) + + it('skips Sunday → Monday', () => { + expect(nextWorkingDay(SUN, cfgMonFri)).toBe(MON2) + }) + + it('skips configured holidays', () => { + const cfg: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [MON, TUE] } + expect(nextWorkingDay(MON, cfg)).toBe(WED) + }) + + it('returns the input midnight unchanged after 60 fruitless iterations', () => { + // No working days configured anywhere — safety bail returns input midnight. + expect(nextWorkingDay(MON, cfgEmpty)).toBe(MON) + }) +}) + +describe('addWorkingDays', () => { + it('Friday + 1 working day = next Monday', () => { + expect(addWorkingDays(FRI, 1, cfgMonFri)).toBe(MON2) + }) + + it('Monday + 0 working days = Monday (no auto-snap)', () => { + expect(addWorkingDays(MON, 0, cfgMonFri)).toBe(MON) + }) + + it('Saturday + 0 working days = Saturday (preserves user-pinned non-working date)', () => { + expect(addWorkingDays(SAT, 0, cfgMonFri)).toBe(SAT) + }) + + it('Monday + 5 working days = next Monday (one full work-week)', () => { + expect(addWorkingDays(MON, 5, cfgMonFri)).toBe(MON2) + }) + + it('Friday + 5 working days = Friday in week after next', () => { + expect(addWorkingDays(FRI, 5, cfgMonFri)).toBe(Date.UTC(2026, 4, 29)) + }) + + it('Monday - 1 working day = previous Friday', () => { + expect(addWorkingDays(MON, -1, cfgMonFri)).toBe(Date.UTC(2026, 4, 15)) + }) + + it('handles a holiday in the middle of the span', () => { + // Mon-Fri active, but Wed is a holiday. + // Mon + 3 wd should land on Fri (skipping the holiday). + const cfg: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [WED] } + expect(addWorkingDays(MON, 3, cfg)).toBe(FRI) + }) + + it('all-days-active: addWorkingDays equals calendar arithmetic', () => { + expect(addWorkingDays(MON, 7, cfgAllDays)).toBe(MON + 7 * DAY_MS) + }) + + it('negative across a weekend: Tuesday - 2 wd = previous Friday', () => { + expect(addWorkingDays(TUE, -2, cfgMonFri)).toBe(Date.UTC(2026, 4, 15)) + }) +}) + +describe('workingDaysBetween', () => { + it('inclusive of both endpoints — Mon..Fri = 5', () => { + expect(workingDaysBetween(MON, FRI, cfgMonFri)).toBe(5) + }) + + it('skips a weekend — Fri..next Mon = 2 (Fri + Mon)', () => { + expect(workingDaysBetween(FRI, MON2, cfgMonFri)).toBe(2) + }) + + it('returns a negative count when a > b (mirroring addWorkingDays semantics)', () => { + expect(workingDaysBetween(FRI, MON, cfgMonFri)).toBe(-5) + }) + + it('returns 1 for the same working day at both endpoints', () => { + expect(workingDaysBetween(MON, MON, cfgMonFri)).toBe(1) + }) + + it('returns 0 for the same non-working day at both endpoints', () => { + expect(workingDaysBetween(SAT, SAT, cfgMonFri)).toBe(0) + }) + + it('counts a holiday as non-working', () => { + const cfg: WorkingCalendar = { weekdayMask: 0b0011111, holidays: [WED] } + expect(workingDaysBetween(MON, FRI, cfg)).toBe(4) + }) +}) + +describe('FS/SS/FF/SF anchors — legacy mode (cfg=undefined)', () => { + it('fsAnchor with lag=0 returns predDue + 1 day (the off-by-one fix)', () => { + const predDue = Date.UTC(2026, 4, 5) + expect(fsAnchor(predDue, 0, undefined)).toBe(Date.UTC(2026, 4, 6)) + }) + + it('fsAnchor with lag=2 returns predDue + 3 days (1 + lag in calendar days)', () => { + const predDue = Date.UTC(2026, 4, 5) + expect(fsAnchor(predDue, 2, undefined)).toBe(Date.UTC(2026, 4, 8)) + }) + + it('ssAnchor with lag=0 returns predStart unchanged (same start)', () => { + const predStart = Date.UTC(2026, 4, 5) + expect(ssAnchor(predStart, 0, undefined)).toBe(predStart) + }) + + it('ffAnchor with lag=1 adds one calendar day to predDue', () => { + const predDue = Date.UTC(2026, 4, 5) + expect(ffAnchor(predDue, 1, undefined)).toBe(Date.UTC(2026, 4, 6)) + }) + + it('sfAnchor with lag=0 returns predStart unchanged', () => { + const predStart = Date.UTC(2026, 4, 5) + expect(sfAnchor(predStart, 0, undefined)).toBe(predStart) + }) + + it('fsReverseAnchor with lag=0 returns succStart - 1 day', () => { + const succStart = Date.UTC(2026, 4, 6) + expect(fsReverseAnchor(succStart, 0, undefined)).toBe(Date.UTC(2026, 4, 5)) + }) + + it('ssReverseAnchor mirrors ssAnchor', () => { + const succStart = Date.UTC(2026, 4, 8) + expect(ssReverseAnchor(succStart, 3, undefined)).toBe(Date.UTC(2026, 4, 5)) + }) + + it('ffReverseAnchor mirrors ffAnchor', () => { + const succDue = Date.UTC(2026, 4, 6) + expect(ffReverseAnchor(succDue, 1, undefined)).toBe(Date.UTC(2026, 4, 5)) + }) + + it('sfReverseAnchor mirrors sfAnchor', () => { + const succDue = Date.UTC(2026, 4, 6) + expect(sfReverseAnchor(succDue, 1, undefined)).toBe(Date.UTC(2026, 4, 5)) + }) +}) + +describe('FS/SS/FF/SF anchors — working-days mode (Mon-Fri)', () => { + it('fsAnchor skips the weekend: Fri + 1+0 wd = next Monday', () => { + expect(fsAnchor(FRI, 0, cfgMonFri)).toBe(MON2) + }) + + it('fsAnchor with lag=2 from Friday lands on Wednesday', () => { + // Fri + (1 + 2) wd = Wed in next week. + expect(fsAnchor(FRI, 2, cfgMonFri)).toBe(Date.UTC(2026, 4, 27)) + }) + + it('ssAnchor with lag=3 from Friday lands on Wednesday next week', () => { + expect(ssAnchor(FRI, 3, cfgMonFri)).toBe(Date.UTC(2026, 4, 27)) + }) + + it('ffAnchor with lag=0 returns predDue unchanged even if predDue is non-working', () => { + // Predecessor stored with a non-working Due → reverse anchor preserves it. + expect(ffAnchor(SAT, 0, cfgMonFri)).toBe(SAT) + }) + + it('fsReverseAnchor from Monday + lag=0 lands on the previous Friday', () => { + expect(fsReverseAnchor(MON2, 0, cfgMonFri)).toBe(FRI) + }) +}) diff --git a/packages/gantt/src/__tests__/y-viewport-perf.test.ts b/packages/gantt/src/__tests__/y-viewport-perf.test.ts new file mode 100644 index 00000000000..e89d3846701 --- /dev/null +++ b/packages/gantt/src/__tests__/y-viewport-perf.test.ts @@ -0,0 +1,84 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Synthetic perf gate. Generates 1000 rows in memory and + * sweeps `computeYViewport` + `sliceVisibleRows` over 100 scroll positions + * (≈ a full top-to-bottom scroll). Asserts the total wall-clock stays + * under 30 ms on the CI box. The pure-logic path is the worst-case render + * cost of one paint frame's virtualization decision — anything above + * single-digit ms here is a regression that would show up as a scroll-fps + * drop on a real device. + */ + +import { computeYViewport, sliceVisibleRows } from '../y-viewport' + +interface SynthRow { + id: string + y: number + height: number +} + +function genRows (count: number, rowHeight: number): SynthRow[] { + const rows: SynthRow[] = new Array(count) + for (let i = 0; i < count; i++) { + rows[i] = { id: `row-${i}`, y: i * rowHeight, height: rowHeight } + } + return rows +} + +describe('y-viewport perf', () => { + it('1000-row × 100-scroll-position sweep finishes well under one frame', () => { + const rowCount = 1000 + const rowHeight = 36 + const viewportHeight = 720 + const rows = genRows(rowCount, rowHeight) + const totalHeight = rowCount * rowHeight + const step = Math.floor(totalHeight / 100) + + let sliceSizeAccum = 0 + const start = Date.now() + for (let i = 0; i < 100; i++) { + const scrollTop = i * step + const v = computeYViewport({ rowCount, rowHeight, scrollTop, viewportHeight }) + const slice = sliceVisibleRows(rows, { top: scrollTop, bottom: scrollTop + viewportHeight }) + sliceSizeAccum += slice.length + v.visibleRange.endIndex - v.visibleRange.startIndex + } + const elapsed = Date.now() - start + + // Modern CPUs do this in single-digit ms; cap at 500 ms so the test + // doesn't flake under a heavily-loaded rush parallel test runner. + // The check exists to flag *catastrophic* regressions (10× slowdown), + // not to gate on sub-frame precision — that's the job of the smoke + // benchmark, not Jest. + expect(elapsed).toBeLessThan(500) + // Defensive: make sure the work wasn't optimized away. + expect(sliceSizeAccum).toBeGreaterThan(0) + }) + + it('5000-row sweep also finishes within one frame budget (16 ms)', () => { + const rowCount = 5000 + const rowHeight = 36 + const viewportHeight = 720 + const rows = genRows(rowCount, rowHeight) + const totalHeight = rowCount * rowHeight + const step = Math.floor(totalHeight / 50) + + const start = Date.now() + let sum = 0 + for (let i = 0; i < 50; i++) { + const scrollTop = i * step + const v = computeYViewport({ rowCount, rowHeight, scrollTop, viewportHeight }) + const slice = sliceVisibleRows(rows, { top: scrollTop, bottom: scrollTop + viewportHeight }) + sum += slice.length + v.visibleRange.endIndex - v.visibleRange.startIndex + } + const elapsed = Date.now() - start + + // 50 sweeps over 5000 rows in ≤ 1500 ms = 30 ms/tick worst-case; + // single-digit ms under no load, generous slack for rush parallelism. + expect(elapsed).toBeLessThan(1500) + expect(sum).toBeGreaterThan(0) + }) +}) diff --git a/packages/gantt/src/__tests__/y-viewport.test.ts b/packages/gantt/src/__tests__/y-viewport.test.ts new file mode 100644 index 00000000000..9e37e07332f --- /dev/null +++ b/packages/gantt/src/__tests__/y-viewport.test.ts @@ -0,0 +1,187 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { computeYViewport, rowIndexToY, sliceVisibleRows, yToRowIndex } from '../y-viewport' + +describe('computeYViewport', () => { + it('returns empty range for zero rows', () => { + const v = computeYViewport({ rowCount: 0, rowHeight: 32, scrollTop: 0, viewportHeight: 320 }) + expect(v.visibleRange).toEqual({ startIndex: 0, endIndex: 0 }) + expect(v.totalSize).toBe(0) + expect(v.paddingTop).toBe(0) + expect(v.paddingBottom).toBe(0) + }) + + it('top of list applies overscan only to the bottom', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: 0, viewportHeight: 320, overscan: 2 }) + expect(v.visibleRange.startIndex).toBe(0) + expect(v.visibleRange.endIndex).toBe(12) // 10 rows fit + 2 overscan + expect(v.paddingTop).toBe(0) + expect(v.paddingBottom).toBe((100 - 12) * 32) + }) + + it('mid-list scroll applies symmetric overscan', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: 160, viewportHeight: 320, overscan: 2 }) + expect(v.visibleRange.startIndex).toBe(3) // 5 - 2 + expect(v.visibleRange.endIndex).toBe(17) // 5 + 10 + 2 + expect(v.paddingTop).toBe(3 * 32) + expect(v.paddingBottom).toBe((100 - 17) * 32) + }) + + it('clamps end to rowCount when overscan overflows', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: 2880, viewportHeight: 320, overscan: 2 }) + expect(v.visibleRange.endIndex).toBe(100) + expect(v.paddingBottom).toBe(0) + }) + + it('clamps negative scrollTop (rubber-band) to 0', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: -50, viewportHeight: 320, overscan: 2 }) + expect(v.visibleRange.startIndex).toBe(0) + expect(v.paddingTop).toBe(0) + }) + + it('defaults overscan to 5 when omitted', () => { + const v = computeYViewport({ rowCount: 200, rowHeight: 32, scrollTop: 320, viewportHeight: 320 }) + // top-row = 10, viewportRows = 10 → endIndex = 10 + 10 + 5 = 25 + // startIndex = 10 - 5 = 5 + expect(v.visibleRange.startIndex).toBe(5) + expect(v.visibleRange.endIndex).toBe(25) + }) + + it('handles a viewport larger than the entire list', () => { + const v = computeYViewport({ rowCount: 5, rowHeight: 32, scrollTop: 0, viewportHeight: 1000, overscan: 2 }) + expect(v.visibleRange.startIndex).toBe(0) + expect(v.visibleRange.endIndex).toBe(5) + expect(v.paddingTop).toBe(0) + expect(v.paddingBottom).toBe(0) + }) + + it('returns full range when rowHeight is non-positive (defensive)', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 0, scrollTop: 0, viewportHeight: 320, overscan: 2 }) + expect(v.visibleRange.startIndex).toBe(0) + expect(v.visibleRange.endIndex).toBe(100) + expect(v.totalSize).toBe(0) + }) + + it('totalSize equals rowCount × rowHeight', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: 0, viewportHeight: 320 }) + expect(v.totalSize).toBe(3200) + }) + + it('paddingTop + visible height + paddingBottom equals totalSize', () => { + const v = computeYViewport({ rowCount: 100, rowHeight: 32, scrollTop: 200, viewportHeight: 320, overscan: 2 }) + const visibleHeight = (v.visibleRange.endIndex - v.visibleRange.startIndex) * 32 + expect(v.paddingTop + visibleHeight + v.paddingBottom).toBe(v.totalSize) + }) +}) + +describe('rowIndexToY', () => { + it('row 0 sits at y=0', () => { + expect(rowIndexToY(0, 32)).toBe(0) + }) + + it('multiplies index × rowHeight', () => { + expect(rowIndexToY(10, 32)).toBe(320) + expect(rowIndexToY(7, 28)).toBe(196) + }) + + it('clamps negative indices to 0', () => { + expect(rowIndexToY(-3, 32)).toBe(0) + }) +}) + +describe('yToRowIndex', () => { + it('y=0 maps to row 0', () => { + expect(yToRowIndex(0, 32, 100)).toBe(0) + }) + + it('floors fractional rows', () => { + expect(yToRowIndex(159, 32, 100)).toBe(4) // 159/32 = 4.96875 → floor 4 + }) + + it('clamps to totalRows - 1 when y overflows', () => { + expect(yToRowIndex(99999, 32, 100)).toBe(99) + }) + + it('clamps negative y to 0', () => { + expect(yToRowIndex(-50, 32, 100)).toBe(0) + }) + + it('returns 0 for empty list (degenerate)', () => { + expect(yToRowIndex(100, 32, 0)).toBe(0) + }) + + it('treats non-positive rowHeight as no-op (returns 0)', () => { + expect(yToRowIndex(100, 0, 100)).toBe(0) + }) +}) + +describe('sliceVisibleRows', () => { + it('returns empty slice for empty input', () => { + expect(sliceVisibleRows([], { top: 0, bottom: 100 })).toEqual([]) + }) + + it('returns rows fully inside the bounds', () => { + const rows = [ + { id: 'a', y: 0, height: 32 }, + { id: 'b', y: 32, height: 32 }, + { id: 'c', y: 64, height: 32 } + ] + const out = sliceVisibleRows(rows, { top: 0, bottom: 100 }) + expect(out).toHaveLength(3) + }) + + it('returns rows straddling the top edge', () => { + const rows = [ + { id: 'a', y: 0, height: 32 }, + { id: 'b', y: 32, height: 32 } + ] + // viewport [16, 64) — row 'a' straddles top, row 'b' fully inside + const out = sliceVisibleRows(rows, { top: 16, bottom: 64 }) + expect(out.map((r) => r.id)).toEqual(['a', 'b']) + }) + + it('returns rows straddling the bottom edge', () => { + const rows = [ + { id: 'a', y: 0, height: 32 }, + { id: 'b', y: 32, height: 32 }, + { id: 'c', y: 64, height: 32 } + ] + // viewport [0, 80) — row 'c' straddles bottom (64..96) + const out = sliceVisibleRows(rows, { top: 0, bottom: 80 }) + expect(out.map((r) => r.id)).toEqual(['a', 'b', 'c']) + }) + + it('excludes rows entirely above viewport', () => { + const rows = [ + { id: 'a', y: 0, height: 32 }, + { id: 'b', y: 32, height: 32 }, + { id: 'c', y: 64, height: 32 } + ] + const out = sliceVisibleRows(rows, { top: 100, bottom: 200 }) + expect(out).toHaveLength(0) + }) + + it('excludes rows entirely below viewport', () => { + const rows = [ + { id: 'a', y: 1000, height: 32 }, + { id: 'b', y: 1032, height: 32 } + ] + const out = sliceVisibleRows(rows, { top: 0, bottom: 100 }) + expect(out).toHaveLength(0) + }) + + it('boundary: row exactly at bottom edge is excluded (y === bounds.bottom)', () => { + const rows = [{ id: 'a', y: 100, height: 32 }] + const out = sliceVisibleRows(rows, { top: 0, bottom: 100 }) + expect(out).toHaveLength(0) + }) + + it('boundary: row exactly meeting top edge is included (y + height > bounds.top)', () => { + const rows = [{ id: 'a', y: 0, height: 32 }] + const out = sliceVisibleRows(rows, { top: 16, bottom: 64 }) + expect(out).toHaveLength(1) + }) +}) diff --git a/packages/gantt/src/__tests__/zoom-dropdown.test.ts b/packages/gantt/src/__tests__/zoom-dropdown.test.ts new file mode 100644 index 00000000000..c13a656c476 --- /dev/null +++ b/packages/gantt/src/__tests__/zoom-dropdown.test.ts @@ -0,0 +1,94 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + dropdownSelectionForPxPerDay, + visibleDaysFromPxPerDay, + pxPerDayFromVisibleDays, + EPSILON_PPD, + MAX_VISIBLE_DAYS, + MIN_VISIBLE_DAYS +} from '../zoom-dropdown' +import { ZOOM_PX_PER_DAY, MIN_PPD } from '../zoom' + +describe('dropdownSelectionForPxPerDay', () => { + it('returns the active preset when userPxPerDay is null', () => { + expect(dropdownSelectionForPxPerDay(null, 'day')).toBe('day') + expect(dropdownSelectionForPxPerDay(null, 'week')).toBe('week') + expect(dropdownSelectionForPxPerDay(null, 'month')).toBe('month') + expect(dropdownSelectionForPxPerDay(null, 'quarter')).toBe('quarter') + }) + + it('returns the matching preset when userPxPerDay equals a preset value', () => { + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.day, 'week')).toBe('day') + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.week, 'day')).toBe('week') + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.month, 'day')).toBe('month') + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.quarter, 'day')).toBe('quarter') + }) + + it('tolerates tiny float-noise within EPSILON_PPD of a preset', () => { + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.day + EPSILON_PPD / 2, 'day')).toBe('day') + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.week - EPSILON_PPD / 2, 'day')).toBe('week') + }) + + it('returns custom for a non-preset userPxPerDay', () => { + expect(dropdownSelectionForPxPerDay(20, 'day')).toBe('custom') + expect(dropdownSelectionForPxPerDay(8, 'week')).toBe('custom') + expect(dropdownSelectionForPxPerDay(0.5, 'quarter')).toBe('custom') + }) + + it('returns custom for values outside the EPSILON_PPD tolerance around a preset', () => { + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.day + 0.5, 'day')).toBe('custom') + expect(dropdownSelectionForPxPerDay(ZOOM_PX_PER_DAY.week + 0.5, 'week')).toBe('custom') + }) +}) + +describe('visibleDaysFromPxPerDay', () => { + it('returns floor of viewport / pxPerDay for typical values', () => { + expect(visibleDaysFromPxPerDay(1200, 32)).toBe(38) // day preset + expect(visibleDaysFromPxPerDay(1200, 14)).toBe(86) // week preset + expect(visibleDaysFromPxPerDay(1200, 4)).toBe(300) // month preset + expect(visibleDaysFromPxPerDay(1200, 1.5)).toBe(800) // quarter preset + }) + + it('returns at least 1 day', () => { + expect(visibleDaysFromPxPerDay(10, 1000)).toBe(1) + expect(visibleDaysFromPxPerDay(0, 32)).toBe(1) + }) + + it('defends against NaN / negative inputs', () => { + expect(visibleDaysFromPxPerDay(Number.NaN, 32)).toBe(1) + expect(visibleDaysFromPxPerDay(1200, Number.NaN)).toBe(1) + expect(visibleDaysFromPxPerDay(1200, 0)).toBe(1) + expect(visibleDaysFromPxPerDay(1200, -5)).toBe(1) + expect(visibleDaysFromPxPerDay(-1200, 32)).toBe(1) + }) +}) + +describe('pxPerDayFromVisibleDays', () => { + it('inverts visibleDaysFromPxPerDay for round-trip', () => { + const viewport = 1200 + const days = 60 + const ppd = pxPerDayFromVisibleDays(viewport, days) + expect(ppd).toBeCloseTo(viewport / days, 5) + expect(visibleDaysFromPxPerDay(viewport, ppd)).toBe(days) + }) + + it('clamps days to [MIN_VISIBLE_DAYS, MAX_VISIBLE_DAYS]', () => { + expect(pxPerDayFromVisibleDays(1200, 0)).toBe(1200 / MIN_VISIBLE_DAYS) + expect(pxPerDayFromVisibleDays(1200, -5)).toBe(1200 / MIN_VISIBLE_DAYS) + expect(pxPerDayFromVisibleDays(1200, 100000)).toBe(1200 / MAX_VISIBLE_DAYS) + }) + + it('defends against NaN/0 viewport (returns MIN_PPD)', () => { + expect(pxPerDayFromVisibleDays(0, 60)).toBe(MIN_PPD) + expect(pxPerDayFromVisibleDays(Number.NaN, 60)).toBe(MIN_PPD) + expect(pxPerDayFromVisibleDays(-100, 60)).toBe(MIN_PPD) + }) + + it('defends against NaN days (treats as MIN_VISIBLE_DAYS)', () => { + expect(pxPerDayFromVisibleDays(1200, Number.NaN)).toBe(1200 / MIN_VISIBLE_DAYS) + }) +}) diff --git a/packages/gantt/src/__tests__/zoom.test.ts b/packages/gantt/src/__tests__/zoom.test.ts new file mode 100644 index 00000000000..1d09cee95dd --- /dev/null +++ b/packages/gantt/src/__tests__/zoom.test.ts @@ -0,0 +1,209 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + adaptiveWheelFactor, + applyWheelZoom, + clampPxPerDay, + cursorAnchoredScrollLeft, + MAX_PPD, + MIN_PPD, + presetForPxPerDay, + pxPerDayToTickZoom, + ZOOM_PX_PER_DAY +} from '../zoom' + +describe('clampPxPerDay', () => { + it('passes valid values through', () => { + expect(clampPxPerDay(10)).toBe(10) + expect(clampPxPerDay(MIN_PPD)).toBe(MIN_PPD) + expect(clampPxPerDay(MAX_PPD)).toBe(MAX_PPD) + }) + + it('clamps too-small values', () => { + expect(clampPxPerDay(0.001)).toBe(MIN_PPD) + expect(clampPxPerDay(0)).toBe(MIN_PPD) + }) + + it('clamps too-large values', () => { + expect(clampPxPerDay(1000)).toBe(MAX_PPD) + }) + + it('rejects NaN / Infinity / negative', () => { + expect(clampPxPerDay(Number.NaN)).toBe(MIN_PPD) + expect(clampPxPerDay(Number.POSITIVE_INFINITY)).toBe(MAX_PPD) + expect(clampPxPerDay(-5)).toBe(MIN_PPD) + }) +}) + +describe('applyWheelZoom', () => { + it('zero deltaY is a no-op', () => { + expect(applyWheelZoom(10, 0)).toBe(10) + }) + + it('negative deltaY zooms in (px/day grows)', () => { + expect(applyWheelZoom(10, -100)).toBeGreaterThan(10) + }) + + it('positive deltaY zooms out (px/day shrinks)', () => { + expect(applyWheelZoom(10, 100)).toBeLessThan(10) + }) + + it('two opposite steps return to (approximately) start', () => { + const start = 14 + const stepUp = applyWheelZoom(start, -100) + const round = applyWheelZoom(stepUp, 100) + expect(round).toBeCloseTo(start, 5) + }) + + it('clamps result at MAX_PPD when zooming in hard', () => { + expect(applyWheelZoom(180, -10000)).toBe(MAX_PPD) + }) + + it('clamps result at MIN_PPD when zooming out hard', () => { + expect(applyWheelZoom(1, 100000)).toBe(MIN_PPD) + }) + + it('multiplies exponentially: equal deltas multiply by equal factors', () => { + const a = applyWheelZoom(10, -100) + const b = applyWheelZoom(20, -100) + expect(a / 10).toBeCloseTo(b / 20, 5) + }) +}) + +describe('cursorAnchoredScrollLeft', () => { + it('keeps the date under the cursor anchored after zoom-in', () => { + // World-x for the date under cursor before zoom: + // xWorld = (scrollLeft + cursorX) / oldPpd + // After cursor-anchored zoom, the new scrollLeft must satisfy: + // (newScrollLeft + cursorX) / newPpd === xWorld + const oldScrollLeft = 100 + const cursorX = 300 + const oldPpd = 10 + const newPpd = 20 + + const newScrollLeft = cursorAnchoredScrollLeft(cursorX, oldScrollLeft, oldPpd, newPpd) + const xWorldOld = (oldScrollLeft + cursorX) / oldPpd + const xWorldNew = (newScrollLeft + cursorX) / newPpd + expect(xWorldNew).toBeCloseTo(xWorldOld, 6) + }) + + it('keeps the date under the cursor anchored after zoom-out', () => { + const oldScrollLeft = 5000 + const cursorX = 600 + const oldPpd = 32 + const newPpd = 8 + + const newScrollLeft = cursorAnchoredScrollLeft(cursorX, oldScrollLeft, oldPpd, newPpd) + const xWorldOld = (oldScrollLeft + cursorX) / oldPpd + const xWorldNew = (newScrollLeft + cursorX) / newPpd + expect(xWorldNew).toBeCloseTo(xWorldOld, 6) + }) + + it('clamps scrollLeft at 0 (no scrolling past start)', () => { + const result = cursorAnchoredScrollLeft(50, 0, 32, 4) + expect(result).toBeGreaterThanOrEqual(0) + }) + + it('returns original when oldPpd is invalid', () => { + expect(cursorAnchoredScrollLeft(100, 200, 0, 10)).toBe(200) + expect(cursorAnchoredScrollLeft(100, 200, Number.NaN, 10)).toBe(200) + }) + + it('returns original when newPpd is invalid', () => { + expect(cursorAnchoredScrollLeft(100, 200, 10, 0)).toBe(200) + expect(cursorAnchoredScrollLeft(100, 200, 10, Number.NaN)).toBe(200) + }) +}) + +describe('pxPerDayToTickZoom', () => { + it('returns day at preset day-pxPerDay', () => { + expect(pxPerDayToTickZoom(ZOOM_PX_PER_DAY.day)).toBe('day') + expect(pxPerDayToTickZoom(50)).toBe('day') + }) + + it('returns week at preset week-pxPerDay', () => { + expect(pxPerDayToTickZoom(ZOOM_PX_PER_DAY.week)).toBe('week') + expect(pxPerDayToTickZoom(10)).toBe('week') + }) + + it('returns month at preset month-pxPerDay', () => { + expect(pxPerDayToTickZoom(ZOOM_PX_PER_DAY.month)).toBe('month') + expect(pxPerDayToTickZoom(3)).toBe('month') + }) + + it('returns quarter at preset quarter-pxPerDay', () => { + expect(pxPerDayToTickZoom(ZOOM_PX_PER_DAY.quarter)).toBe('quarter') + expect(pxPerDayToTickZoom(0.5)).toBe('quarter') + expect(pxPerDayToTickZoom(0.1)).toBe('quarter') + }) + + it('handles invalid values gracefully (returns quarter)', () => { + expect(pxPerDayToTickZoom(0)).toBe('quarter') + expect(pxPerDayToTickZoom(-1)).toBe('quarter') + expect(pxPerDayToTickZoom(Number.NaN)).toBe('quarter') + }) + + it('boundaries: > 20 → day, == 20 → week', () => { + expect(pxPerDayToTickZoom(20.01)).toBe('day') + expect(pxPerDayToTickZoom(20)).toBe('week') + }) + + it('boundaries: > 7 → week, == 7 → month', () => { + expect(pxPerDayToTickZoom(7.01)).toBe('week') + expect(pxPerDayToTickZoom(7)).toBe('month') + }) + + it('boundaries: > 2 → month, == 2 → quarter', () => { + expect(pxPerDayToTickZoom(2.01)).toBe('month') + expect(pxPerDayToTickZoom(2)).toBe('quarter') + }) +}) + +describe('presetForPxPerDay', () => { + it('mirrors pxPerDayToTickZoom for toolbar highlight', () => { + expect(presetForPxPerDay(32)).toBe('day') + expect(presetForPxPerDay(14)).toBe('week') + expect(presetForPxPerDay(4)).toBe('month') + expect(presetForPxPerDay(1.5)).toBe('quarter') + }) +}) + +describe('adaptiveWheelFactor', () => { + it('uses 0.006 for week and day densities (>= 4 ppd)', () => { + expect(adaptiveWheelFactor(32)).toBe(0.006) // day preset + expect(adaptiveWheelFactor(14)).toBe(0.006) // week preset + expect(adaptiveWheelFactor(4)).toBe(0.006) // month preset boundary + }) + + it('uses 0.012 for month and quarter densities (< 4 ppd)', () => { + expect(adaptiveWheelFactor(3.99)).toBe(0.012) // just under boundary + expect(adaptiveWheelFactor(1.5)).toBe(0.012) // quarter preset + expect(adaptiveWheelFactor(0.1)).toBe(0.012) // very low density + }) + + it('defaults to 0.012 on invalid input', () => { + expect(adaptiveWheelFactor(0)).toBe(0.012) + expect(adaptiveWheelFactor(-5)).toBe(0.012) + expect(adaptiveWheelFactor(Number.NaN)).toBe(0.012) + }) + + it('makes low-density zoom subjectively faster than fixed 0.006 would', () => { + // One wheel notch (deltaY=100) at quarter density (ppd=1.5): + // fixed 0.006: ppd * exp(-0.6) ≈ ppd * 0.549 → ~45 % zoom-out + // adaptive 0.012: ppd * exp(-1.2) ≈ ppd * 0.301 → ~70 % zoom-out + const oldPpd = 1.5 + const fixed = applyWheelZoom(oldPpd, 100, 0.006) + const adaptive = applyWheelZoom(oldPpd, 100) // adaptive default + expect(adaptive).toBeLessThan(fixed) + }) + + it('produces same zoom in high-density (week=14)', () => { + // Adaptive factor for ppd=14 is 0.006, identical to the fixed value + const adaptive = applyWheelZoom(14, 100) + const fixed = applyWheelZoom(14, 100, 0.006) + expect(adaptive).toBe(fixed) + }) +}) diff --git a/packages/gantt/src/breakpoint.ts b/packages/gantt/src/breakpoint.ts new file mode 100644 index 00000000000..d5a01c088d9 --- /dev/null +++ b/packages/gantt/src/breakpoint.ts @@ -0,0 +1,45 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Mobile-Friendly Gantt — layout-mode detection. + * + * Pure helper module: maps a viewport width (typically `window.innerWidth`) + * to one of the three mode buckets the Gantt UI cares about: + * + * - `phone` : ≤640 px — sidebar collapses to a slide-out drawer, the + * canvas is read-only (no drag, no connector-create, no + * editor popups). + * - `tablet` : 641–1024 px — full Edit-Mode but touch-aware: drag needs a + * long-press, hit-targets are enlarged. + * - `desktop`: >1024 px — legacy Desktop behaviour bit-for-bit. + * + * Defensive: any non-positive or NaN width is treated as `phone` so the + * safest (read-only) mode wins when the caller hasn't measured yet. + */ + +export type LayoutMode = 'phone' | 'tablet' | 'desktop' + +export const PHONE_MAX_WIDTH = 640 +export const TABLET_MAX_WIDTH = 1024 + +export function detectLayoutMode (width: number): LayoutMode { + if (!Number.isFinite(width) || width <= 0) return 'phone' + if (width <= PHONE_MAX_WIDTH) return 'phone' + if (width <= TABLET_MAX_WIDTH) return 'tablet' + return 'desktop' +} + +export function isPhone (mode: LayoutMode): boolean { + return mode === 'phone' +} + +export function isTablet (mode: LayoutMode): boolean { + return mode === 'tablet' +} + +export function isDesktop (mode: LayoutMode): boolean { + return mode === 'desktop' +} diff --git a/packages/gantt/src/cascade-popup-layout.ts b/packages/gantt/src/cascade-popup-layout.ts new file mode 100644 index 00000000000..f9dd6723c9f --- /dev/null +++ b/packages/gantt/src/cascade-popup-layout.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * fix — pure layout helpers for {@link ConfirmCascadePopup}. + * + * The previous `bodyHeight = min(rows * ROW_HEIGHT + BAR_TOP_PADDING, + * BODY_MAX_HEIGHT)` formula matched the svg's content height exactly but + * forgot the `.body` element's CSS padding (4 px top + 4 px bottom), so + * the bottom edge of the last row was clipped by the scroller — visible + * as a sliced-off bar in the "3 issues will be shifted" test. + * + * This helper folds the padding + a small safety margin into the height + * so the popup grows to fit the typical N<10 case without scrolling, + * and only scrolls once the rendered timeline genuinely exceeds the + * BODY_MAX_HEIGHT ceiling. + */ +export interface CascadePopupLayoutInput { + rowCount: number + rowHeight: number + barTopPadding: number + bodyVerticalPadding: number + bodyBottomSafety: number + bodyMaxHeight: number +} + +export function computeCascadeBodyHeight (input: CascadePopupLayoutInput): number { + const desired = + input.rowCount * input.rowHeight + input.barTopPadding + input.bodyVerticalPadding + input.bodyBottomSafety + return Math.min(desired, input.bodyMaxHeight) +} diff --git a/packages/gantt/src/cascade-token.ts b/packages/gantt/src/cascade-token.ts new file mode 100644 index 00000000000..00a964f88ec --- /dev/null +++ b/packages/gantt/src/cascade-token.ts @@ -0,0 +1,61 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * CascadeToken plumbing. + * + * Every cascade-related commit in the Gantt (single-drag, parent-drag, + * bypass and the soon-to-arrive bulk-drag from ) attaches + * a fresh token to the `client.apply(undefined, scope)` call. The token + * is the `scope` string, so downstream consumers can correlate every + * sub-Tx of the same cascade by scope-string equality: + * + * • (Bulk-Drag) — all bulk-moved primaries plus their + * cascade fanout share one token, so the dependency-graph layer + * can build a single visual undo entry per bulk operation instead + * of N entries (which would over-count in the undo stack). + * + * • (Dependency-Shift-Notification) — the cascade-commit + * in GanttView.svelte forwards the freshly-minted token to + * `sendDependencyShiftedNotifications`, which uses it as the + * `cascadeToken` field on every `DependencyShiftedNotification` so + * downstream consumers can correlate the bundle with the Tx batch. + * The token is *also* the `TxApplyIf.scope` of the commit; a future + * server-side trigger could read it to suppress the auto-generated + * `dueDate` notifications, but that pathway is consumed inside + * `ApplyTxMiddleware` today (see `dependency-shift-send.ts` notes for + * details) — so v1 emits the bundle as a sibling notification rather + * than replacing the generic ping. + * + * The token is intentionally a *string* and not a Tx-Mixin, because: + * 1. No schema change → no migration cost. + * 2. `TxApplyIf.scope` already round-trips through the server (it + * drives the optimistic-concurrency match-set), so it is visible + * to triggers without extra plumbing. + * 3. Plays nicely with `client.apply()` overloads that already accept + * a scope string everywhere in this codebase. + * + * The suffix is `Date.now()` plus a monotonic counter — collision-free + * within one client process, unique enough across processes for the + * notification batching window (~minutes). If we ever need cross-server + * uniqueness, swap the suffix for a v4 UUID without touching the call + * sites; the prefix contract stays. + */ + +let counter = 0 + +/** + * Returns a fresh cascade-token usable as the `scope` argument of + * `client.apply(undefined, scope)`. The default prefix is `gantt-cascade`; + * specialized call sites pass their own scope (`gantt-cascade-commit`, + * `gantt-cascade-bypass`, `gantt-no-cascade`, `gantt-bulk-drag`, …) so + * that the leading segment of the scope still classifies the call site + * for human-readable tx-logs, while the trailing `:` segment + * identifies the cascade instance. + */ +export function newCascadeToken (prefix: string = 'gantt-cascade'): string { + counter = (counter + 1) | 0 + return `${prefix}:${Date.now()}-${counter}` +} diff --git a/packages/gantt/src/confirm-gate.ts b/packages/gantt/src/confirm-gate.ts new file mode 100644 index 00000000000..02c27278feb --- /dev/null +++ b/packages/gantt/src/confirm-gate.ts @@ -0,0 +1,44 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * fix — drag-commit confirmation gate. + * + * Background. After a Gantt bar drag ends, the user-configurable + * confirmation popup (GanttConfirmCommitPopup or ConfirmCascadePopup) + * runs as a non-modal showPopup. The window-level pointer listeners that + * GanttView attaches while the drag is active stay wired the whole time + * the popup is open — which produced two bugs reported in * + * 1. Hover-bug: pointermove kept calling the drag reducer, so the bar + * visually trailed the cursor while the popup was up. + * 2. Double-popup-bug: clicking the popup's Cancel/Apply button bubbled + * pointerup to window, which re-entered handleCanvasPointerUp while + * activeDrag was still in `dragging-body` — opening a second popup. + * + * This module is a tiny module-scope flag. GanttView toggles it on + * before showing a confirmation popup and off when the popup resolves. + * The pointer handlers consult {@link isConfirming} to short-circuit. + * + * The flag is module-scope (not store-based) on purpose — there is only + * ever one drag in flight, the consumer is GanttView, and the flag is + * always set/cleared inside the same async commit path. A Svelte store + * would force a reactive cycle through `$:` blocks for a guard that + * needs to be read synchronously inside event handlers. + */ + +let confirming = false + +export function setConfirming (value: boolean): void { + confirming = value +} + +export function isConfirming (): boolean { + return confirming +} + +/** Test-only — reset flag between specs so a leaked-true doesn't bleed. */ +export function resetConfirmGate (): void { + confirming = false +} diff --git a/packages/gantt/src/cp-scheduler.ts b/packages/gantt/src/cp-scheduler.ts new file mode 100644 index 00000000000..afda1cb25fb --- /dev/null +++ b/packages/gantt/src/cp-scheduler.ts @@ -0,0 +1,52 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Debounce handle for the critical-path / slack recompute. + * + * The forward/backward pass behind `computeCriticalPath` is triggered by a + * reactive statement that fires on every issue, relation and view-option + * change — dozens of times per second while a bar is being dragged. The + * recompute is therefore debounced: only the last change in a burst runs. + * + * The handle lives in its own object rather than as a bare `setTimeout` id in + * the component because a pending timer has to be *cancelled*, not just + * ignored, in two situations: + * + * - both consumers (critical-path overlay and slack column) get switched off + * while a recompute is already queued. Leaving the timer armed would let it + * fire afterwards, repopulate the result the toggle just cleared, and even + * raise the "dependency cycle" warning for a view that shows neither; + * - the view unmounts, where a late callback would write to torn-down state. + * + * `cancel()` is idempotent and safe to call when nothing is pending. + */ +export class DebouncedRecompute { + private handle: ReturnType | null = null + + constructor (private readonly delayMs: number) {} + + /** Replace any pending run with a new one `delayMs` from now. */ + schedule (run: () => void): void { + this.cancel() + this.handle = setTimeout(() => { + this.handle = null + run() + }, this.delayMs) + } + + /** Drop a pending run, if any. Idempotent. */ + cancel (): void { + if (this.handle !== null) { + clearTimeout(this.handle) + this.handle = null + } + } + + /** True while a run is queued. */ + get pending (): boolean { + return this.handle !== null + } +} diff --git a/packages/gantt/src/drag-controller.ts b/packages/gantt/src/drag-controller.ts new file mode 100644 index 00000000000..9f76c0d0ef8 --- /dev/null +++ b/packages/gantt/src/drag-controller.ts @@ -0,0 +1,281 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { DragEvent, DragState, DragTarget, GanttItem } from './types' +import { snapToUtcMidnight } from './time-scale' +import type { TimeScale } from './time-scale' + +/** + * Pure reduction over drag state. Given the current state and an input event + * (mouse or cancel), returns the next state. No IO; no DOM access; the time + * scale is passed in as a value so the reducer is fully deterministic and + * trivially testable. + * + * The reducer is doc-agnostic: it operates only on origin / preview dates + * and the captured drag target. `target.kind` (issue vs milestone) is + * threaded through unchanged so commitDrag (in GanttView.svelte) can route + * to the right update field. + */ +export function reduce ( + state: DragState, + event: DragEvent, + timeScale: TimeScale +): DragState { + // The reducer is doc-agnostic: it only copies the `target` / `source` / + // `hoveredBar` payloads through and reads their `_id`. Running the concrete + // implementation and re-asserting the generic parameters is therefore sound — + // whatever specific target/node the caller fed in is exactly what comes back. + return reduceImpl(state as unknown as DragState, event as unknown as DragEvent, timeScale) as unknown as DragState< + TTarget, + TNode + > +} + +function reduceImpl (state: DragState, event: DragEvent, timeScale: TimeScale): DragState { + switch (state.kind) { + case 'idle': + return reduceFromIdle(state, event) + case 'hover-bar': + return reduceFromHover(state, event, timeScale) + case 'dragging-body': + case 'dragging-unscheduled': + case 'resizing-left': + case 'resizing-right': + return reduceFromActive(state, event, timeScale) + case 'connector-drawing': + case 'connector-target-hover': + return reduceFromConnector(state, event) + } +} + +function reduceFromIdle (state: DragState & { kind: 'idle' }, event: DragEvent): DragState { + if (event.type === 'mouseenter-bar') { + return { kind: 'hover-bar', issueId: event.issueId, edge: event.edge } + } + // Allow direct idle → drag in one step. Real users always pass through + // hover-bar first (mouseenter fires before mousedown), but synthetic event + // dispatch (Playwright tests) and edge-cases where the bar is summoned under + // the cursor (e.g. after a re-render) can skip the hover state. Treat + // mousedown-bar as the start of a drag regardless. + if (event.type === 'mousedown-bar') { + const base = { + target: event.target, + originStart: event.originStart, + originEnd: event.originEnd, + cursorStartX: event.cursorX + } + if (event.edge === 'body') { + // when the mousedown carries a co-drag payload we + // mount it into the dragging-body state with anchorDeltaMs = 0. The + // reducer's mousemove branch maintains it from there. + return { + kind: 'dragging-body', + ...base, + previewStart: event.originStart, + previewEnd: event.originEnd, + ...(event.coDrag !== undefined + ? { + coDrag: { + anchorDeltaMs: 0, + members: event.coDrag.members, + minDeltaMs: event.coDrag.minDeltaMs, + maxDeltaMs: event.coDrag.maxDeltaMs + } + } + : {}) + } + } + if (event.edge === 'left') { + return { kind: 'resizing-left', ...base, previewStart: event.originStart } + } + if (event.edge === 'right') { + return { kind: 'resizing-right', ...base, previewEnd: event.originEnd } + } + } + if (event.type === 'mousedown-unscheduled') { + // Default to "today" for both dates; cursor movement during the drag + // shifts them in lockstep just like dragging-body. originStart/originEnd + // are recorded so commitDrag() and the resize-overlay can share the same + // code path as dragging-body. hasCanvasTarget starts false — only a real + // mousemove with canvasX flips it true and unlocks the commit. + const today = snapToUtcMidnight(Date.now()) + return { + kind: 'dragging-unscheduled', + target: event.target, + originStart: today, + originEnd: today + 86_400_000, + cursorStartX: event.cursorX, + previewStart: today, + previewEnd: today + 86_400_000, + hasCanvasTarget: false + } + } + if (event.type === 'mousedown-connector') { + return { + kind: 'connector-drawing', + source: event.source, + originPx: event.originPx, + cursorPx: event.cursorPx + } + } + return state +} + +function reduceFromHover (state: DragState & { kind: 'hover-bar' }, event: DragEvent, timeScale: TimeScale): DragState { + if (event.type === 'mouseleave-bar') { + return { kind: 'idle' } + } + if (event.type === 'mousedown-bar') { + const base = { + target: event.target, + originStart: event.originStart, + originEnd: event.originEnd, + cursorStartX: event.cursorX + } + if (event.edge === 'body') { + return { + kind: 'dragging-body', + ...base, + previewStart: event.originStart, + previewEnd: event.originEnd, + ...(event.coDrag !== undefined + ? { + coDrag: { + anchorDeltaMs: 0, + members: event.coDrag.members, + minDeltaMs: event.coDrag.minDeltaMs, + maxDeltaMs: event.coDrag.maxDeltaMs + } + } + : {}) + } + } + if (event.edge === 'left') { + return { kind: 'resizing-left', ...base, previewStart: event.originStart } + } + if (event.edge === 'right') { + return { kind: 'resizing-right', ...base, previewEnd: event.originEnd } + } + } + if (event.type === 'mousedown-connector') { + return { + kind: 'connector-drawing', + source: event.source, + originPx: event.originPx, + cursorPx: event.cursorPx + } + } + void timeScale + return state +} + +function reduceFromActive (state: DragState, event: DragEvent, timeScale: TimeScale): DragState { + if (event.type === 'mouseup' || event.type === 'cancel') { + return { kind: 'idle' } + } + if (event.type !== 'mousemove') return state + + if (state.kind === 'dragging-body') { + const deltaPx = event.cursorX - state.cursorStartX + const rawDeltaMs = (deltaPx / timeScale.pxPerDay) * 86_400_000 + // when a co-drag is active, clamp the raw delta to the + // shared min/max window — the hard-stop semantic. Snap + // is computed against the clamped delta so the entire group lands on + // identical UTC-midnight boundaries. + if (state.coDrag !== undefined) { + const clampedDeltaMs = Math.max(state.coDrag.minDeltaMs, Math.min(state.coDrag.maxDeltaMs, rawDeltaMs)) + const previewStart = snapToUtcMidnight(state.originStart + clampedDeltaMs) + const previewEnd = snapToUtcMidnight(state.originEnd + clampedDeltaMs) + // anchorDeltaMs tracks the snapped delta from the leading bar so that + // every other member's preview = origin + anchorDeltaMs lands on the + // same midnight grid. + const anchorDeltaMs = previewStart - state.originStart + return { + ...state, + previewStart, + previewEnd, + coDrag: { ...state.coDrag, anchorDeltaMs } + } + } + return { + ...state, + previewStart: snapToUtcMidnight(state.originStart + rawDeltaMs), + previewEnd: snapToUtcMidnight(state.originEnd + rawDeltaMs) + } + } + + if (state.kind === 'resizing-left') { + const deltaPx = event.cursorX - state.cursorStartX + const deltaMs = (deltaPx / timeScale.pxPerDay) * 86_400_000 + const candidate = snapToUtcMidnight(state.originStart + deltaMs) + // Clamp so previewStart never crosses originEnd (would invert the bar). + return { ...state, previewStart: Math.min(candidate, state.originEnd) } + } + + if (state.kind === 'resizing-right') { + const deltaPx = event.cursorX - state.cursorStartX + const deltaMs = (deltaPx / timeScale.pxPerDay) * 86_400_000 + const candidate = snapToUtcMidnight(state.originEnd + deltaMs) + return { ...state, previewEnd: Math.max(candidate, state.originStart) } + } + + if (state.kind === 'dragging-unscheduled') { + // Only update the preview when the cursor is actually over the canvas. + if (event.canvasX === undefined) return state + const newStart = snapToUtcMidnight(timeScale.fromX(event.canvasX)) + return { + ...state, + previewStart: newStart, + previewEnd: newStart + 86_400_000, + hasCanvasTarget: true + } + } + + return state +} + +function reduceFromConnector ( + state: DragState & { kind: 'connector-drawing' | 'connector-target-hover' }, + event: DragEvent +): DragState { + if (event.type === 'mouseup-connector' || event.type === 'cancel') { + return { kind: 'idle' } + } + if (event.type !== 'mousemove-connector') return state + + // Drag self → keep drawing (a relation from a bar to itself is meaningless; + // we don't even attempt a target-hover state, so wouldCreateCycle never + // sees that edge). + if (event.hoveredBar !== null && event.hoveredBar._id === state.source._id) { + return { + kind: 'connector-drawing', + source: state.source, + originPx: state.originPx, + cursorPx: event.cursorPx + } + } + + if (event.hoveredBar === null) { + return { + kind: 'connector-drawing', + source: state.source, + originPx: state.originPx, + cursorPx: event.cursorPx + } + } + + return { + kind: 'connector-target-hover', + source: state.source, + originPx: state.originPx, + cursorPx: event.cursorPx, + target: event.hoveredBar + } +} + +/** Convenience helper used by later tasks to clamp a date to a UTC midnight. */ +export function snapDate (t: number): number { + return snapToUtcMidnight(t) +} diff --git a/packages/gantt/src/drag-state.ts b/packages/gantt/src/drag-state.ts new file mode 100644 index 00000000000..fde6da7eea3 --- /dev/null +++ b/packages/gantt/src/drag-state.ts @@ -0,0 +1,21 @@ +import type { DragState, DragTarget } from './types' + +export function activeDragTargetId (state: DragState): string | null { + if (!('target' in state)) return null + + const target = state.target + if (!isDragTarget(target)) return null + + const id = target.doc._id + return id !== undefined ? String(id) : null +} + +function isDragTarget (target: unknown): target is DragTarget { + return ( + typeof target === 'object' && + target !== null && + 'doc' in target && + typeof (target as { doc?: unknown }).doc === 'object' && + (target as { doc?: unknown }).doc !== null + ) +} diff --git a/packages/gantt/src/flash-store.ts b/packages/gantt/src/flash-store.ts new file mode 100644 index 00000000000..fc4eb650f1e --- /dev/null +++ b/packages/gantt/src/flash-store.ts @@ -0,0 +1,77 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Visual feedback store for undo/redo. + * + * GanttBar / GanttDependencyArrow subscribe to this store; entries listed in + * the Set get a transient highlight (gepunktete outline). The UndoManager + * pushes IDs into the store on successful undo/redo, and the timer below + * removes them after `durationMs`. + * + * Per-id timers are tracked so that overlapping flashes don't truncate each + * other: a second `flashIssues(['x'], …)` while `x` is already flashing + * cancels the pending timeout and schedules a fresh one starting from the + * second call. + * + * The store implements the minimal Svelte contract (`subscribe`) so that + * GanttView / GanttBar can use `$flashStore` syntax — without dragging in + * `svelte/store`, which is ESM-only and breaks jest-ts-jest in this repo. + */ +export interface FlashStore { + subscribe: (run: (value: Set) => void) => () => void + get: () => Set + // internal — exposed for the flashIssues helper + set: (value: Set) => void +} + +const timers = new WeakMap>>() + +export function createFlashStore (): FlashStore { + let value = new Set() + const subs = new Set<(value: Set) => void>() + const store: FlashStore = { + subscribe (run) { + subs.add(run) + run(value) + return () => { + subs.delete(run) + } + }, + get () { + return value + }, + set (next) { + value = next + for (const fn of subs) fn(value) + } + } + timers.set(store, new Map()) + return store +} + +export function flashIssues (ids: string[], durationMs: number, store: FlashStore): void { + const perStoreTimers = timers.get(store) ?? new Map>() + timers.set(store, perStoreTimers) + + const cur = store.get() + const next = new Set(cur) + for (const id of ids) next.add(id) + store.set(next) + + for (const id of ids) { + const existing = perStoreTimers.get(id) + if (existing !== undefined) clearTimeout(existing) + const handle = setTimeout(() => { + perStoreTimers.delete(id) + const c = store.get() + if (!c.has(id)) return + const n = new Set(c) + n.delete(id) + store.set(n) + }, durationMs) + perStoreTimers.set(id, handle) + } +} diff --git a/packages/gantt/src/gantt-view-options.ts b/packages/gantt/src/gantt-view-options.ts new file mode 100644 index 00000000000..ce875b2d48d --- /dev/null +++ b/packages/gantt/src/gantt-view-options.ts @@ -0,0 +1,123 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// +// Saved Gantt views. +// Pure (de)serialization helpers for the Gantt-specific keys we tunnel +// through `FilteredView.viewOptions` (which is itself a +// `Record`): +// +// ganttZoomLevel : 'day' | 'week' | 'month' | 'quarter' (required) +// ganttPanAnchorDate?: 'YYYY-MM-DD' (UTC midnight; only written when the +// user ticks the "pin time window" +// checkbox while saving the view) +// +// No schema migration is needed — `FilteredView.viewOptions` is open. + +import type { ZoomLevel, BarColorMode } from './types' + +const ZOOM_LEVELS: readonly ZoomLevel[] = ['day', 'week', 'month', 'quarter'] +const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/ + +export interface GanttSavedViewOptions { + zoomLevel: ZoomLevel + /** ISO 'YYYY-MM-DD' UTC-midnight anchor, only set when the user fixes the time window. */ + panAnchorDate?: string + ganttBarColorBy?: BarColorMode + ganttShowPastDueOverlay?: boolean + ganttShowBlockedOverlay?: boolean + ganttShowSubIssueProgress?: boolean +} + +function isZoomLevel (v: unknown): v is ZoomLevel { + return typeof v === 'string' && (ZOOM_LEVELS as readonly string[]).includes(v) +} + +function isValidIsoDate (v: unknown): v is string { + if (typeof v !== 'string' || !ISO_DATE_RE.test(v)) return false + const t = Date.parse(v + 'T00:00:00Z') + return Number.isFinite(t) +} + +const BAR_COLOR_MODES: readonly BarColorMode[] = ['status', 'priority', 'assignee', 'component', 'milestone', 'none'] + +/** Type-guard for a persisted bar-colour mode. Exported so GanttView can reuse it. */ +export function isBarColorMode (v: unknown): v is BarColorMode { + return typeof v === 'string' && (BAR_COLOR_MODES as readonly string[]).includes(v) +} + +/** Read the Gantt-specific keys back out of a (possibly mixed) viewOptions blob. */ +export function extractGanttSavedView (raw: Record | undefined): GanttSavedViewOptions { + if (raw == null) return { zoomLevel: 'week' } + const zoom = isZoomLevel(raw.ganttZoomLevel) ? raw.ganttZoomLevel : 'week' + const out: GanttSavedViewOptions = { zoomLevel: zoom } + if (isValidIsoDate(raw.ganttPanAnchorDate)) { + out.panAnchorDate = raw.ganttPanAnchorDate + } + if (isBarColorMode(raw.ganttBarColorBy)) { + out.ganttBarColorBy = raw.ganttBarColorBy + } + if (typeof raw.ganttShowPastDueOverlay === 'boolean') { + out.ganttShowPastDueOverlay = raw.ganttShowPastDueOverlay + } + if (typeof raw.ganttShowBlockedOverlay === 'boolean') { + out.ganttShowBlockedOverlay = raw.ganttShowBlockedOverlay + } + if (typeof raw.ganttShowSubIssueProgress === 'boolean') { + out.ganttShowSubIssueProgress = raw.ganttShowSubIssueProgress + } + return out +} + +/** + * Write the Gantt-specific keys into a viewOptions blob without mutating the + * caller's object. Unrelated keys (ganttShowTitle, ganttConfirmMove, …) are + * preserved. If `panAnchorDate` is absent in the payload, any stale + * `ganttPanAnchorDate` in the base is dropped — pinning the time window + * unchecked must clear a previously-saved anchor. + */ +export function mergeGanttSavedView ( + base: Record | undefined, + opts: GanttSavedViewOptions +): Record { + const out: Record = { ...(base ?? {}) } + out.ganttZoomLevel = opts.zoomLevel + if (opts.panAnchorDate !== undefined) { + out.ganttPanAnchorDate = opts.panAnchorDate + } else { + delete out.ganttPanAnchorDate + } + + // Bar-color toolbar state. Only persist when the caller explicitly + // supplied a value — undefined means "use existing value if any". We + // intentionally do NOT delete on undefined; the loader's active-default + // reset handles missing keys. + if (isBarColorMode(opts.ganttBarColorBy)) { + out.ganttBarColorBy = opts.ganttBarColorBy + } + if (typeof opts.ganttShowPastDueOverlay === 'boolean') { + out.ganttShowPastDueOverlay = opts.ganttShowPastDueOverlay + } + if (typeof opts.ganttShowBlockedOverlay === 'boolean') { + out.ganttShowBlockedOverlay = opts.ganttShowBlockedOverlay + } + if (typeof opts.ganttShowSubIssueProgress === 'boolean') { + out.ganttShowSubIssueProgress = opts.ganttShowSubIssueProgress + } + return out +} + +/** Format a millisecond timestamp as 'YYYY-MM-DD' (UTC). */ +export function isoDateForTimestamp (t: number): string { + const d = new Date(t) + const yyyy = d.getUTCFullYear().toString().padStart(4, '0') + const mm = (d.getUTCMonth() + 1).toString().padStart(2, '0') + const dd = d.getUTCDate().toString().padStart(2, '0') + return `${yyyy}-${mm}-${dd}` +} + +/** Parse 'YYYY-MM-DD' to UTC-midnight ms. Returns NaN on malformed input. */ +export function timestampForIsoDate (iso: string): number { + if (!ISO_DATE_RE.test(iso)) return Number.NaN + return Date.parse(iso + 'T00:00:00Z') +} diff --git a/packages/gantt/src/index.ts b/packages/gantt/src/index.ts new file mode 100644 index 00000000000..0ad795e2110 --- /dev/null +++ b/packages/gantt/src/index.ts @@ -0,0 +1,40 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +// Domain-neutral Gantt engine: scheduling, zoom, drag/pointer, time-scale and +// viewport primitives with no dependency on any platform package. + +export * from './types' +export * from './breakpoint' +export * from './cascade-popup-layout' +export * from './cascade-token' +export * from './confirm-gate' +export * from './cp-scheduler' +export * from './drag-controller' +export * from './drag-state' +export * from './flash-store' +export * from './gantt-view-options' +// `initial` is re-exported from './pinch-zoom' (consumed as `pinchInitial`); +// long-press's own `initial` is only used inside its unit test (which imports it +// directly), so it is intentionally left out of the barrel to avoid the clash. +export { + LONG_PRESS_MS, + MOVE_THRESHOLD_PX, + reduceLongPress, + type LongPressState, + type LongPressEvent +} from './long-press' +export * from './pan-target' +export * from './pinch-zoom' +export * from './pointer-classify' +export * from './sidebar-columns' +export * from './time-scale' +export * from './toolbar-overflow' +export * from './tree-expand-store' +export * from './viewport' +export * from './working-days' +export * from './y-viewport' +export * from './zoom-dropdown' +export * from './zoom' diff --git a/packages/gantt/src/long-press.ts b/packages/gantt/src/long-press.ts new file mode 100644 index 00000000000..3a835f361a6 --- /dev/null +++ b/packages/gantt/src/long-press.ts @@ -0,0 +1,67 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Mobile-Friendly Gantt — long-press timer state machine. + * + * Pure reducer used by the touch-aware drag entry-points on Tablet/Desktop. + * The DOM layer feeds it `start` / `tick` / `move` / `cancel` events and + * reads back the discriminated state — no setTimeout/Date.now leaks into + * this module, so it's trivially testable with synthetic timestamps. + * + * idle → start(now, x, y) → pending(startedAt=now, x, y) + * pending → tick(now ≥ start+300) → fired + * pending → move(>10 px from x,y) → cancelled + * pending → cancel() → idle + * fired → tick / move → fired (idempotent) + * cancelled → cancel/tick → cancelled (terminal) + * + * On tablets a 300 ms long-press on a bar enters selection mode, which keeps + * a plain touch free for scrolling the canvas. + * Movement-threshold (10 px) borrowed from MS-Touch — below this the user + * is "holding still", above they're trying to scroll. + */ + +export const LONG_PRESS_MS = 300 +export const MOVE_THRESHOLD_PX = 10 + +export type LongPressState = + | { kind: 'idle' } + | { kind: 'pending', startedAt: number, x: number, y: number } + | { kind: 'fired' } + | { kind: 'cancelled' } + +export type LongPressEvent = + | { type: 'start', now: number, x: number, y: number } + | { type: 'tick', now: number } + | { type: 'move', now: number, x: number, y: number } + | { type: 'cancel' } + +export function initial (): LongPressState { + return { kind: 'idle' } +} + +export function reduceLongPress (state: LongPressState, event: LongPressEvent): LongPressState { + switch (event.type) { + case 'start': + if (state.kind === 'pending') return state // second start while pending is a no-op + return { kind: 'pending', startedAt: event.now, x: event.x, y: event.y } + case 'tick': + if (state.kind !== 'pending') return state + if (event.now - state.startedAt >= LONG_PRESS_MS) return { kind: 'fired' } + return state + case 'move': + if (state.kind !== 'pending') return state + { + const dx = event.x - state.x + const dy = event.y - state.y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist > MOVE_THRESHOLD_PX) return { kind: 'cancelled' } + } + return state + case 'cancel': + return { kind: 'idle' } + } +} diff --git a/packages/gantt/src/pan-target.ts b/packages/gantt/src/pan-target.ts new file mode 100644 index 00000000000..8c48473432c --- /dev/null +++ b/packages/gantt/src/pan-target.ts @@ -0,0 +1,34 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Elements that own an explicit interaction and must not start the canvas-pan + * gesture. Unselected Gantt bars are intentionally not excluded: a short click + * selects the bar, while click-and-hold/drag on that same unselected bar pans + * the timeline just like dragging on empty canvas. Once selected, the bar body + * and resize handles become explicit edit controls and are excluded. + */ +export const PAN_EXCLUDED_SELECTOR = [ + '.sidebar-cell', + '.drag-grip', + 'button', + 'a', + '.toggle-btn', + '.jump-btn', + '.resize-cell', + '.gantt-connector-dot', + 'rect.bar.selected', + '.summary-hit.selected', + '.bar-resize-handle' +].join(', ') + +export function shouldStartCanvasPan (target: Pick | null): boolean { + if (target === null) return false + return target.closest(PAN_EXCLUDED_SELECTOR) === null +} + +export function shouldPromoteCanvasPan (dx: number, dy: number, threshold = 3): boolean { + return Math.abs(dx) > threshold || Math.abs(dy) > threshold +} diff --git a/packages/gantt/src/pinch-zoom.ts b/packages/gantt/src/pinch-zoom.ts new file mode 100644 index 00000000000..cd165182774 --- /dev/null +++ b/packages/gantt/src/pinch-zoom.ts @@ -0,0 +1,147 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { MIN_PPD, MAX_PPD } from './zoom' + +/** + * Mobile-Friendly Gantt — pinch-zoom tracker. + * + * Pure pointer-tracking reducer for 2-finger pinch on the Gantt canvas. + * The DOM layer wires it to `pointerdown` / `pointermove` / `pointerup` + * / `pointercancel` (iOS Safari scroll-inertia case) and reads back the + * current `distance` ratio to drive `userPxPerDay` via + * `computePxPerDayFromRatio()`. + * + * Design: + * - Native 2-finger pinch zooms the Time-Scale, NOT a CSS-Scale. + * - Zoom anchor = midpoint between the two fingers AT pinch-start. + * - Pinch-in (fingers together) → zoom out (ratio < 1, ppd shrinks). + * - Pinch-out (fingers apart) → zoom in (ratio > 1, ppd grows). + */ + +export interface Point { + x: number + y: number +} + +export type PinchState = + | { kind: 'idle' } + | { kind: 'single', id: number, x: number, y: number } + | { + kind: 'pinch' + idA: number + idB: number + a: Point + b: Point + center: Point + initialDistance: number + currentDistance: number + initialPxPerDay: number + } + +export type PinchEvent = + | { type: 'down', id: number, x: number, y: number, pxPerDay: number } + | { type: 'move', id: number, x: number, y: number } + | { type: 'up', id: number } + | { type: 'cancel' } + +export function initial (): PinchState { + return { kind: 'idle' } +} + +export function computeDistance (a: Point, b: Point): number { + const dx = b.x - a.x + const dy = b.y - a.y + return Math.sqrt(dx * dx + dy * dy) +} + +export function computeCenter (a: Point, b: Point): Point { + return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } +} + +/** + * Map a pinch distance-ratio to a new px-per-day, clamped to the same + * [MIN_PPD, MAX_PPD] range that Ctrl+Wheel-zoom uses: the finger distance + * delta maps onto a px-per-day ratio exactly like a wheel delta does, so + * touch and mouse zoom end up at the same scale. + * + * Defensive: non-finite or zero ratios are treated as "no change". + */ +export function computePxPerDayFromRatio (initialPpd: number, ratio: number): number { + if (!Number.isFinite(ratio) || ratio <= 0) return initialPpd + if (!Number.isFinite(initialPpd) || initialPpd <= 0) return MIN_PPD + const next = initialPpd * ratio + if (next < MIN_PPD) return MIN_PPD + if (next > MAX_PPD) return MAX_PPD + return next +} + +export function reducePinch (state: PinchState, event: PinchEvent): PinchState { + switch (event.type) { + case 'down': + return onDown(state, event) + case 'move': + return onMove(state, event) + case 'up': + return onUp(state, event) + case 'cancel': + return { kind: 'idle' } + } +} + +function onDown (state: PinchState, event: Extract): PinchState { + if (state.kind === 'idle') { + return { kind: 'single', id: event.id, x: event.x, y: event.y } + } + if (state.kind === 'single') { + const a: Point = { x: state.x, y: state.y } + const b: Point = { x: event.x, y: event.y } + const dist = computeDistance(a, b) + return { + kind: 'pinch', + idA: state.id, + idB: event.id, + a, + b, + center: computeCenter(a, b), + initialDistance: dist, + currentDistance: dist, + initialPxPerDay: event.pxPerDay + } + } + // Already pinching — ignore third+ touches (Spec: only 2-finger gesture). + return state +} + +function onMove (state: PinchState, event: Extract): PinchState { + if (state.kind === 'single') { + if (state.id !== event.id) return state + return { ...state, x: event.x, y: event.y } + } + if (state.kind === 'pinch') { + if (event.id !== state.idA && event.id !== state.idB) return state + const a = event.id === state.idA ? { x: event.x, y: event.y } : state.a + const b = event.id === state.idB ? { x: event.x, y: event.y } : state.b + return { ...state, a, b, currentDistance: computeDistance(a, b) } + } + return state +} + +function onUp (state: PinchState, event: Extract): PinchState { + if (state.kind === 'single') { + if (state.id !== event.id) return state + return { kind: 'idle' } + } + if (state.kind === 'pinch') { + if (event.id === state.idA) { + return { kind: 'single', id: state.idB, x: state.b.x, y: state.b.y } + } + if (event.id === state.idB) { + return { kind: 'single', id: state.idA, x: state.a.x, y: state.a.y } + } + return state + } + return state +} diff --git a/packages/gantt/src/pointer-classify.ts b/packages/gantt/src/pointer-classify.ts new file mode 100644 index 00000000000..e91e3d96b4b --- /dev/null +++ b/packages/gantt/src/pointer-classify.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { LayoutMode } from './breakpoint' + +/** + * Mobile-Friendly Gantt — pointer-classification. + * + * Given the current layout mode, the originating PointerEvent's + * `pointerType` and the user-visible action ("what is the user trying to + * do"), return whether the action should be allowed directly, blocked + * outright (read-only Phone mode), or gated behind a long-press timer + * (Touch on Tablet/Desktop). + * + * The classifier is the single source of truth for the + * full-edit-on-tablet / read-only-on-phone decision. Keeping it as a pure + * function lets unit tests cover every cell of the matrix without + * jsdom's partial Touch-Event support. + */ + +export type PointerKind = PointerEvent['pointerType'] | 'mouse' | 'touch' | 'pen' | '' +export type PointerAction = 'tap' | 'drag' | 'resize' | 'connector' +export type PointerDecision = 'allow' | 'long-press' | 'block' + +export function classifyPointer (mode: LayoutMode, pointerType: PointerKind, action: PointerAction): PointerDecision { + if (mode === 'phone') { + // Phone is strictly read-only regardless of input device. Only `tap` + // (open Quick-Info / activate Sidebar drawer) is allowed. + return action === 'tap' ? 'allow' : 'block' + } + + if (action === 'tap') return 'allow' + + // Tablet + Desktop: pen/mouse are direct; touch requires a long-press + // confirmation (a 300 ms long-press enters selection mode), so a plain + // touch stays available for scrolling. + if (pointerType === 'touch') return 'long-press' + return 'allow' +} diff --git a/packages/gantt/src/sidebar-columns.ts b/packages/gantt/src/sidebar-columns.ts new file mode 100644 index 00000000000..5aa138ef40b --- /dev/null +++ b/packages/gantt/src/sidebar-columns.ts @@ -0,0 +1,162 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Sidebar inline-grid columns. + * + * Pure helpers for the configurable Gantt sidebar column system. All inputs + * are runtime-unknown (ViewOption storage) so the parsing path is defensive: + * unknown shapes degrade gracefully to {@link DEFAULT_COLUMNS}. Width values + * are clamped between {@link MIN_WIDTH} and {@link MAX_WIDTH} on every entry + * The resize handle in the header writes through `clampWidth`, so even a + * misbehaving pointer (or a tampered preference) cannot push the sidebar + * into an unusable state. + */ + +/** Union of all built-in column keys. */ +export type SidebarColumnKey = + | 'identifier' + | 'title' + | 'status' + | 'priority' + | 'assignee' + | 'estimation' + | 'component' + | 'milestone' + | 'predecessors' + | 'slack' + | 'startDate' + | 'dueDate' + | 'deadline' + | 'progress' + | 'modifiedOn' + | 'createdOn' + +/** Authoritative list of every recognised column key, ordered by intuitive UI grouping. */ +export const ALL_COLUMN_KEYS: readonly SidebarColumnKey[] = [ + 'identifier', + 'title', + 'status', + 'priority', + 'assignee', + 'estimation', + 'component', + 'milestone', + 'predecessors', + 'slack', + 'startDate', + 'dueDate', + 'deadline', + 'progress', + 'modifiedOn', + 'createdOn' +] + +/** + * Default visible columns when no user preference exists — preserves the + * pre-Phase-3a sidebar exactly, so existing users see no surface change + * on first upgrade. + */ +export const DEFAULT_COLUMNS: readonly SidebarColumnKey[] = ['identifier', 'title', 'predecessors', 'slack'] + +/** Per-column default pixel width. Tweak in tandem with `.cell-{key}` CSS. */ +export const DEFAULT_WIDTHS: Record = { + identifier: 80, + title: 240, + status: 100, + priority: 80, + assignee: 140, + estimation: 80, + component: 120, + milestone: 140, + predecessors: 140, + slack: 60, + startDate: 100, + dueDate: 100, + deadline: 100, + progress: 80, + modifiedOn: 100, + createdOn: 100 +} + +/** Hard floor — narrower than this and the cell content is unreadable. */ +export const MIN_WIDTH = 40 +/** Hard ceiling — wider than this and the sidebar drowns the canvas. */ +export const MAX_WIDTH = 600 + +const KNOWN_SET: ReadonlySet = new Set(ALL_COLUMN_KEYS) + +/** + * Coerce an unknown ViewOption value into a SidebarColumnKey[]. Filters + * unknown keys, deduplicates, and falls back to {@link DEFAULT_COLUMNS} + * when the input shape is wrong or the resulting list would be empty. + */ +export function parseColumns (raw: unknown): SidebarColumnKey[] { + if (!Array.isArray(raw)) return [...DEFAULT_COLUMNS] + const seen = new Set() + const out: SidebarColumnKey[] = [] + for (const entry of raw) { + if (typeof entry !== 'string') continue + if (!KNOWN_SET.has(entry)) continue + const key = entry as SidebarColumnKey + if (seen.has(key)) continue + seen.add(key) + out.push(key) + } + if (out.length === 0) return [...DEFAULT_COLUMNS] + return out +} + +/** + * Coerce an unknown ViewOption value into a per-column width map. + * + * Column widths are persisted as a ViewOption so a resized Slack / + * Predecessors / Title column survives a viewlet re-mount instead of + * snapping back to {@link DEFAULT_WIDTHS}. Same defensive contract as + * {@link parseColumns}: unknown keys and non-finite / out-of-range values + * are dropped, everything else is clamped, and the result always starts from + * the defaults so a partially-written blob can never leave a column + * width-less. + */ +export function parseWidths (raw: unknown): Record { + const out: Record = { ...DEFAULT_WIDTHS } + if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) return out + for (const [key, value] of Object.entries(raw as Record)) { + if (!KNOWN_SET.has(key)) continue + if (typeof value !== 'number' || !Number.isFinite(value)) continue + out[key as SidebarColumnKey] = clampWidth(value) + } + return out +} + +/** Clamp + round a width value so persisted/dragged values stay sane. */ +export function clampWidth (px: number): number { + if (!Number.isFinite(px)) return MIN_WIDTH + const rounded = Math.round(px) + if (rounded < MIN_WIDTH) return MIN_WIDTH + if (rounded > MAX_WIDTH) return MAX_WIDTH + return rounded +} + +/** + * fix — total pixel width of the visible column set. Used by the + * extended sidebar grid to size the outer container to the actual columns + * sum, so the outer GanttView sidebar grid-column tracks the inner grid + * and the resize handle / column headers / overflow line up. + * + * Per-column overrides that are missing, negative, or non-finite are + * coerced to {@link DEFAULT_WIDTHS} for that column — matches the + * defensive parsing contract in {@link parseColumns}. + */ +export function computeTotalWidth (cols: readonly SidebarColumnKey[], widths: Record): number { + let sum = 0 + for (const c of cols) { + const override = widths[c] + const usable = + typeof override === 'number' && Number.isFinite(override) && override > 0 ? override : DEFAULT_WIDTHS[c] + sum += usable + } + return sum +} diff --git a/packages/gantt/src/time-scale.ts b/packages/gantt/src/time-scale.ts new file mode 100644 index 00000000000..94017e690eb --- /dev/null +++ b/packages/gantt/src/time-scale.ts @@ -0,0 +1,202 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { type Tick, type ZoomLevel } from './types' + +const DAY_MS = 86_400_000 + +const PX_PER_DAY: Record = { + day: 32, + week: 14, + month: 4, + quarter: 1.5 +} + +/** Snap any Timestamp (ms) to the start of its UTC day. */ +export function snapToUtcMidnight (t: number): number { + const d = new Date(t) + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) +} + +/** + * Canonical boundary normalizer for issue/milestone date fields entering the + * Gantt. The Gantt computes exclusively in UTC-midnight (scheduler, CPM, + * non-working-day tint, layout, bar rects). Editors elsewhere may persist a + * local-midnight timestamp; feeding that raw value in causes off-by-one tint + * days and a visible bar-jump on the first drag (drag-commit snaps, read did + * not). Snapping every date at the data boundary makes read and write agree. + * + * `null`/`undefined` (unscheduled / no deadline) map to `null`. + */ +export function toGanttDay (t: number | null | undefined): number | null { + return t == null ? null : snapToUtcMidnight(t) +} + +export interface TimeScale { + /** Pixel width of one calendar day at the current zoom. */ + pxPerDay: number + /** Convert a Timestamp to its X coordinate (relative to origin). */ + toX: (t: number) => number + /** Convert an X coordinate back to a snapped Timestamp. */ + fromX: (x: number) => number + /** Generate header ticks across [from, to]. */ + ticks: (range: [number, number]) => Tick[] +} + +export function createTimeScale (zoom: ZoomLevel, origin: number, pxPerDayOverride?: number): TimeScale { + const pxPerDay = pxPerDayOverride ?? PX_PER_DAY[zoom] + const originSnapped = snapToUtcMidnight(origin) + + const toX = (t: number): number => ((t - originSnapped) / DAY_MS) * pxPerDay + const fromX = (x: number): number => snapToUtcMidnight(originSnapped + (x / pxPerDay) * DAY_MS) + + const ticks = (range: [number, number]): Tick[] => { + const [from, to] = range + const fromDay = snapToUtcMidnight(from) + const result: Tick[] = [] + let cursor = fromDay + + switch (zoom) { + case 'day': { + // Secondary label: month name on the 1st of each month, or on the + // first visible tick (so the user sees a context label even if the + // viewport starts mid-month). + let lastMonth = -1 + let first = true + while (cursor <= to) { + const d = new Date(cursor) + const isMonday = d.getUTCDay() === 1 + const m = d.getUTCMonth() + let secondary: string | undefined + if (first || m !== lastMonth) { + secondary = d.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }) + lastMonth = m + first = false + } + result.push({ + date: cursor, + label: d.getUTCDate().toString(), + level: isMonday ? 'major' : 'minor', + secondaryLabel: secondary + }) + cursor += DAY_MS + } + break + } + case 'week': { + // Secondary label: year on the first week of each year (or first + // visible week). Aligns the supra-row with year boundaries. + const d = new Date(cursor) + const dow = d.getUTCDay() // 0=Sun + const offsetToMonday = (1 - dow + 7) % 7 + cursor += offsetToMonday * DAY_MS + let lastYear = -1 + let first = true + while (cursor <= to) { + const c = new Date(cursor) + const isFirstWeekOfMonth = c.getUTCDate() <= 7 + const y = c.getUTCFullYear() + let secondary: string | undefined + if (first || y !== lastYear) { + secondary = String(y) + lastYear = y + first = false + } + result.push({ + date: cursor, + label: `W${isoWeekNumber(c)}`, + level: isFirstWeekOfMonth ? 'major' : 'minor', + secondaryLabel: secondary + }) + cursor += 7 * DAY_MS + } + break + } + case 'month': { + // Secondary label: year on January (or first visible month). Was + // entirely missing before — without it, you couldn't tell which year + // a month belonged to in a multi-year span. + const start = new Date(cursor) + let y = start.getUTCFullYear() + let m = start.getUTCMonth() + cursor = Date.UTC(y, m, 1) + let lastYear = -1 + let first = true + while (cursor <= to) { + const c = new Date(cursor) + const yr = c.getUTCFullYear() + let secondary: string | undefined + if (first || yr !== lastYear) { + secondary = String(yr) + lastYear = yr + first = false + } + result.push({ + date: cursor, + label: c.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }), + level: c.getUTCMonth() === 0 ? 'major' : 'minor', + secondaryLabel: secondary + }) + m += 1 + if (m > 11) { + m = 0 + y += 1 + } + cursor = Date.UTC(y, m, 1) + } + break + } + case 'quarter': { + // Split the year out of the primary label and put it on the supra + // row — previously the label was "Q1 2026" side-by-side, which is + // visually noisy in a dense Quarter view. + const start = new Date(cursor) + let y = start.getUTCFullYear() + let q = Math.floor(start.getUTCMonth() / 3) + cursor = Date.UTC(y, q * 3, 1) + let lastYear = -1 + let first = true + while (cursor <= to) { + const c = new Date(cursor) + const qNum = Math.floor(c.getUTCMonth() / 3) + 1 + const yr = c.getUTCFullYear() + let secondary: string | undefined + if (first || yr !== lastYear) { + secondary = String(yr) + lastYear = yr + first = false + } + result.push({ + date: cursor, + label: `Q${qNum}`, + level: qNum === 1 ? 'major' : 'minor', + secondaryLabel: secondary + }) + q += 1 + if (q > 3) { + q = 0 + y += 1 + } + cursor = Date.UTC(y, q * 3, 1) + } + break + } + } + + return result + } + + return { pxPerDay, toX, fromX, ticks } +} + +/** ISO 8601 week number for a UTC date. */ +function isoWeekNumber (d: Date): number { + const target = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) + const dayNum = (target.getUTCDay() + 6) % 7 + target.setUTCDate(target.getUTCDate() - dayNum + 3) + const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)) + const diff = target.getTime() - firstThursday.getTime() + return 1 + Math.round(diff / (7 * DAY_MS)) +} diff --git a/packages/gantt/src/toolbar-overflow.ts b/packages/gantt/src/toolbar-overflow.ts new file mode 100644 index 00000000000..75011c7c6dc --- /dev/null +++ b/packages/gantt/src/toolbar-overflow.ts @@ -0,0 +1,119 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Gantt-toolbar overflow — pure collapse arithmetic. + * + * The Gantt toolbar is lifted into the SpaceHeader's second row, where it + * competes for space with the search input, the filter button, the filter + * chips and the trailing Fullscreen / More-actions cluster. Below roughly + * 2400 px of panel width the row simply overflowed: nothing shrank, and the + * trailing cluster was pushed outside the (overflow-hidden) header container, + * i.e. it existed in the DOM but could never be clicked. + * + * The fix has two halves. The layout half lets the search group shrink and + * freezes the trailing group (see `shrinkSearch` on `Header.svelte`). This + * module is the other half: given the natural width of each toolbar tier and + * the width actually available, decide which tiers stay inline and which move + * into the "…" popover. + * + * Keeping the decision in a pure function is what makes it testable at all — + * the DOM side only measures and renders. + */ + +/** + * Toolbar tiers in VISUAL left-to-right order. A tier is the smallest unit + * that collapses as a whole; splitting e.g. the date-navigation into single + * buttons would leave a half-usable control group behind. + */ +export const TOOLBAR_TIERS = ['group', 'nav', 'date', 'zoom', 'undo', 'savedview'] as const +export type ToolbarTier = (typeof TOOLBAR_TIERS)[number] + +/** + * Collapse order, least important first. + * + * - `savedview` is a passive "modified" indicator plus its update button; the + * same action is reachable from the More-actions menu. + * - `date` (jump-to-date) duplicates what panning and the nav buttons do. + * - `group` (group-by / colour-by) is view configuration, not navigation. + * - `undo` matters while editing, but the keyboard shortcuts stay live. + * - `zoom` and `nav` are the two controls a Gantt is unusable without, so + * they are the last to go. + */ +export const TOOLBAR_COLLAPSE_ORDER: readonly ToolbarTier[] = ['savedview', 'date', 'group', 'undo', 'zoom', 'nav'] + +/** + * Width assumed for a tier that has never been measured. Deliberately + * generous: over-estimating collapses one tier too many for a single frame, + * under-estimating leaves the row overflowing — and an overflowing row is + * what put the trailing cluster off-screen in the first place. + */ +export const TIER_FALLBACK_PX = 140 + +export interface ToolbarOverflowResult { + /** Tiers that stay inline, in the order they were passed in. */ + visible: ToolbarTier[] + /** Tiers that move into the "…" popover, in the order they were passed in. */ + hidden: ToolbarTier[] +} + +/** + * Decide the inline/collapsed split. + * + * @param present tiers that currently have content, in visual order + * @param widths measured natural width per tier (px); missing entries fall + * back to {@link TIER_FALLBACK_PX} + * @param available width the toolbar cluster may occupy (px). A NEGATIVE + * value means "not measured yet" and keeps everything + * inline, so the first paint can measure. Zero is a real + * measurement (a fully squeezed cluster) and collapses + * everything — treating it as "unmeasured" would re-expand + * the toolbar into a box it cannot fit into. + * @param gap flex gap between two inline items (px) + * @param moreButtonWidth width the "…" trigger occupies INSIDE the measured + * box (px). Pass 0 when the trigger is rendered elsewhere — + * it is, see `ganttToolbarHiddenTiers` — so the arithmetic + * neither reserves its width nor a gap for it. + */ +export function computeToolbarOverflow ( + present: readonly ToolbarTier[], + widths: Readonly>>, + available: number, + gap: number, + moreButtonWidth: number +): ToolbarOverflowResult { + const all = present.filter((t, i) => present.indexOf(t) === i) + if (!Number.isFinite(available) || available < 0) { + return { visible: [...all], hidden: [] } + } + + const widthOf = (t: ToolbarTier): number => { + const w = widths[t] + return typeof w === 'number' && Number.isFinite(w) && w > 0 ? w : TIER_FALLBACK_PX + } + + const hidden = new Set() + const total = (): number => { + const shown = all.filter((t) => !hidden.has(t)) + let items = shown.length + let sum = shown.reduce((acc, t) => acc + widthOf(t), 0) + if (hidden.size > 0 && moreButtonWidth > 0) { + sum += moreButtonWidth + items += 1 + } + return sum + Math.max(0, items - 1) * gap + } + + for (const tier of TOOLBAR_COLLAPSE_ORDER) { + if (total() <= available) break + if (!all.includes(tier)) continue + hidden.add(tier) + } + + return { + visible: all.filter((t) => !hidden.has(t)), + hidden: all.filter((t) => hidden.has(t)) + } +} diff --git a/packages/gantt/src/tree-expand-store.ts b/packages/gantt/src/tree-expand-store.ts new file mode 100644 index 00000000000..bf373536267 --- /dev/null +++ b/packages/gantt/src/tree-expand-store.ts @@ -0,0 +1,117 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Tree-View — persisted expand/collapse state for the Gantt + * sidebar hierarchy. + * + * Spec decisions implemented here: + * - Persisted **per user** (one localStorage namespace per user-session) AND + * keyed **per project** so cross-project navigation does not bleed state. + * - Default is **expanded** — the persisted value records *only* deviations + * (i.e. the row-ids the user has explicitly collapsed), so an unseen issue + * is implicitly expanded. + * + * Storage backend is the `Storage` interface (browser `localStorage` in + * production, an in-memory shim in unit tests). The spec mentions a future + * migration to preference-service-backed docs; the store-API here is + * deliberately backend-agnostic so the swap is a one-file change. + * + * Failure modes: + * - corrupt JSON / non-array values → treated as "no persisted state" + * (the user simply gets the default-expanded view, no exception thrown). + * - `setItem` throwing (quota exceeded, private mode) → caught and ignored; + * state still updates in-memory and listeners still fire. + */ + +const KEY_PREFIX = 'huly:gantt:tree-collapsed:' + +export interface TreeExpandStore { + /** Current set of row-ids that are collapsed. */ + getCollapsed: () => Set + /** Flip a single id's collapsed-state. */ + toggle: (id: string) => void + /** Replace the entire collapsed-set. */ + setAll: (collapsed: Set) => void + /** Clear all entries (everything expanded). */ + expandAll: () => void + /** Collapse the supplied ids (everything else expanded). */ + collapseAll: (ids: readonly string[]) => void + /** + * Subscribe to state changes. The callback is invoked once synchronously + * with the current snapshot, then again after every mutation. Returns an + * unsubscribe function. + */ + subscribe: (cb: (collapsed: Set) => void) => () => void +} + +function readPersisted (storage: Storage, key: string): Set { + try { + const raw = storage.getItem(key) + if (raw === null || raw === '') return new Set() + const parsed: unknown = JSON.parse(raw) + if (!Array.isArray(parsed)) return new Set() + const out = new Set() + for (const v of parsed) { + if (typeof v === 'string') out.add(v) + } + return out + } catch { + return new Set() + } +} + +function writePersisted (storage: Storage, key: string, value: Set): void { + try { + storage.setItem(key, JSON.stringify([...value])) + } catch { + // quota exceeded / private-mode — silently drop the write; the in-memory + // state still reflects the user's intent for the current session. + } +} + +/** + * Build a fresh store bound to one project. Each call reads the persisted + * value at construction time; later changes from a parallel store on the same + * key would not be observed without a manual reload (acceptable since the + * user typically has a single Gantt tab open per project). + */ +export function createTreeExpandStore (projectId: string, storage: Storage): TreeExpandStore { + const key = KEY_PREFIX + projectId + let current: Set = readPersisted(storage, key) + const listeners = new Set<(c: Set) => void>() + + function commit (next: Set): void { + current = next + writePersisted(storage, key, current) + for (const cb of listeners) cb(current) + } + + return { + getCollapsed: () => current, + toggle: (id) => { + const next = new Set(current) + if (next.has(id)) next.delete(id) + else next.add(id) + commit(next) + }, + setAll: (collapsed) => { + commit(new Set(collapsed)) + }, + expandAll: () => { + commit(new Set()) + }, + collapseAll: (ids) => { + commit(new Set(ids)) + }, + subscribe: (cb) => { + listeners.add(cb) + cb(current) + return () => { + listeners.delete(cb) + } + } + } +} diff --git a/packages/gantt/src/types.ts b/packages/gantt/src/types.ts new file mode 100644 index 00000000000..3867898c077 --- /dev/null +++ b/packages/gantt/src/types.ts @@ -0,0 +1,240 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +// Domain-neutral engine types. This package deliberately imports nothing from +// the platform packages: the scheduling/zoom/drag/viewport engine operates on +// the structural minimum it needs (an item's `_id`, plain date numbers) so it +// can be reused by any consumer. Tracker-specific types (Issue, Milestone, +// IssueRelation, Ref) live in the tracker adapter and are threaded in through +// the generic parameters below. + +/** Zoom presets — controls pxPerDay and header tick density. */ +export type ZoomLevel = 'day' | 'week' | 'month' | 'quarter' + +/** A single tick on the time-axis header (vertical gridline + label). */ +export interface Tick { + date: number // UTC ms + label: string // pre-formatted, locale-aware (primary row) + level: 'major' | 'minor' // major ticks render thicker + with text label + /** + * Optional supra-label rendered on a second header row above `label` when + * the segment changes from the previous tick. Day view sets it to month + * name on the 1st of each month; week view sets it to the year on the + * first week of each year; month view sets it to the year on January; + * quarter view sets it to the year on Q1. + */ + secondaryLabel?: string +} + +/** + * How Gantt bars are colored. Pure string union — the resolver that maps a + * mode + item to concrete colors lives in the tracker adapter (`bar-colors`), + * which re-exports this union from here. + */ +export type BarColorMode = 'status' | 'priority' | 'assignee' | 'component' | 'milestone' | 'none' + +/** + * Structural minimum for anything the engine drags, schedules or lays out. + * A tracker Issue/Milestone satisfies this because their `_id` (a branded + * `Ref<…>`) is assignable to `string`. + */ +export interface GanttItem { + _id: string +} + +/** + * Working-days calendar. Field-for-field structurally identical to the + * tracker's `WorkingDaysConfig`, so a tracker config value is assignable to + * this type without a conversion. + */ +export interface WorkingCalendar { + /** + * Bitmask of active weekdays. + * + * bit 0 = Mon, bit 1 = Tue, …, bit 5 = Sat, bit 6 = Sun. + * + * Mon–Fri = 0b0011111 = 31 + * Mon–Sat = 0b0111111 = 63 + * All days = 0b1111111 = 127 + */ + weekdayMask: number + + /** Holidays as UTC-midnight timestamps. Order is irrelevant; duplicates allowed. */ + holidays: number[] +} + +/** + * Neutral scheduling dependency between two items. This is the stable, domain- + * agnostic contract for a predecessor→successor link; the tracker adapter maps + * its `IssueRelation`-based structures onto it. The dependency-routing and + * critical-path logic themselves remain in the adapter — this interface is the + * shared vocabulary, not the implementation. + */ +export interface GanttDependency { + _id: string + /** Predecessor item id. */ + fromId: string + /** Successor item id. */ + toId: string + /** Finish-to-Start, Start-to-Start, Finish-to-Finish or Start-to-Finish. */ + kind: 'FS' | 'SS' | 'FF' | 'SF' + /** Optional lag in working days (may be negative for lead). */ + lagDays?: number +} + +/** Which part of a bar is being interacted with. */ +export type DragKind = 'body' | 'left' | 'right' | 'unscheduled' + +/** + * Discriminated union of what's being dragged. The engine reducer is + * doc-agnostic — it only uses originStart / originEnd / cursor deltas and the + * item's `_id`; the `target` payload is preserved verbatim so the consumer's + * commit routine can branch on `target.kind` to write the right field and run + * the right cascade. Consumers instantiate `TTarget`/`TNode` (see `DragState`) + * with their own discriminated union to recover full typing at the commit + * boundary; the default keeps the engine usable in a fully neutral form. + */ +export interface DragTarget { + kind: string + doc: D +} + +/** + * Discriminated state of the live drag/resize interaction. + * + * Generic parameters: + * - `TTarget` — the dragged target payload (a `{ kind, doc }` shape). The + * tracker adapter supplies its `{ kind: 'issue', doc: Issue } | { kind: + * 'milestone', doc: Milestone }` union so `commitDrag` can narrow on `kind`. + * - `TNode` — a bare item node used by the connector (dependency-draw) states. + */ +export type DragState = + | { kind: 'idle' } + | { kind: 'hover-bar', issueId: string, edge: 'left' | 'right' | 'body' | 'none' } + | { + kind: 'dragging-body' + target: TTarget + originStart: number + originEnd: number + cursorStartX: number + previewStart: number + previewEnd: number + /** Bulk co-drag state: other selected issues being shifted in sync. */ + coDrag?: { + members: Array<{ issueId: string, originStart: number, originEnd: number }> + minDeltaMs: number + maxDeltaMs: number + anchorDeltaMs: number + } + } + | { + kind: 'resizing-left' + target: TTarget + originStart: number + originEnd: number + cursorStartX: number + previewStart: number + } + | { + kind: 'resizing-right' + target: TTarget + originStart: number + originEnd: number + cursorStartX: number + previewEnd: number + } + | { + kind: 'dragging-unscheduled' + target: TTarget + /** Anchor date the drag was started from (defaults to today at UTC midnight). */ + originStart: number + /** originStart + 1 day; used for ghost-outline / commit symmetry with dragging-body. */ + originEnd: number + cursorStartX: number + previewStart: number + previewEnd: number + /** + * True once the cursor has been over the canvas during the drag and a real + * canvas-X has been observed. Guards against the click-without-drag case + * where mouseup fires before the user has moved over the canvas — committing + * such a "drag" would schedule the issue to today silently. `commitDrag` + * treats `dragging-unscheduled && !hasCanvasTarget` as a no-op. + */ + hasCanvasTarget: boolean + } + | { + kind: 'connector-drawing' + /** Source item the user is drawing the dependency from. */ + source: TNode + /** Pixel x/y of the connector-dot on the source bar (where the curve starts). */ + originPx: { x: number, y: number } + /** Live cursor x/y in canvas-content coordinates (where the curve ends). */ + cursorPx: { x: number, y: number } + } + | { + kind: 'connector-target-hover' + source: TNode + originPx: { x: number, y: number } + cursorPx: { x: number, y: number } + /** Candidate target item under the pointer. */ + target: TNode + } + +/** + * Input events fed into the drag-controller reducer. + * + * Coordinate spaces: + * - `cursorX` is always window-space `MouseEvent.clientX`. It drives delta + * math for `dragging-body` / `resizing-left` / `resizing-right`, which only + * care about *how much* the cursor moved since `mousedown`. + * - `canvasX` (optional on `mousemove`) is the cursor's X position in the + * canvas's content coordinate system — i.e., already accounting for the + * sidebar's left offset and the horizontal scroll. `timeScale.fromX(canvasX)` + * yields the absolute date under the cursor. It is used by + * `dragging-unscheduled` to snap the dropped issue to the date the cursor + * is actually pointing at, not to a delta from "today". + * + * When the cursor is over the sidebar (e.g., at the start of an unscheduled + * drag), `canvasX` is undefined and the unscheduled preview holds its + * default ("today") until the cursor enters the canvas. + */ +export type DragEvent = + | { type: 'mouseenter-bar', issueId: string, edge: 'left' | 'right' | 'body' } + | { type: 'mouseleave-bar' } + | { + type: 'mousedown-bar' + target: TTarget + /** Start / end dates of the target at mousedown; reducer stores these + * as `originStart` / `originEnd` and adds the cursor-delta to compute + * previews. Captured here at the dispatch boundary so the doc-agnostic + * reducer doesn't need to know which field on `target.doc` to read. */ + originStart: number + originEnd: number + edge: 'left' | 'right' | 'body' + cursorX: number + /** Bulk co-drag state: other selected issues to shift in sync. */ + coDrag?: { + members: Array<{ issueId: string, originStart: number, originEnd: number }> + minDeltaMs: number + maxDeltaMs: number + } + } + | { type: 'mousedown-unscheduled', target: TTarget, cursorX: number } + | { type: 'mousemove', cursorX: number, canvasX?: number } + | { type: 'mouseup' } + | { type: 'cancel' } + | { + type: 'mousedown-connector' + source: TNode + originPx: { x: number, y: number } + cursorPx: { x: number, y: number } + } + | { + type: 'mousemove-connector' + cursorPx: { x: number, y: number } + /** Bar under the cursor right now, or null when over empty canvas. */ + hoveredBar: TNode | null + } + | { type: 'mouseup-connector' } diff --git a/packages/gantt/src/viewport.ts b/packages/gantt/src/viewport.ts new file mode 100644 index 00000000000..266faaadce6 --- /dev/null +++ b/packages/gantt/src/viewport.ts @@ -0,0 +1,69 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { WorkingCalendar } from './types' +import { isWorkingDay } from './working-days' + +const DAY_MS = 86_400_000 + +export function computeCanvasViewportWidth ( + scrollerClientWidth: number, + sidebarWidth: number, + resizeCellWidth: number +): number { + return Math.max(1, scrollerClientWidth - sidebarWidth - resizeCellWidth) +} + +export function computeCanvasRenderWidth (dataRangeWidth: number, viewportWidth: number): number { + return Math.max(1, dataRangeWidth, viewportWidth) +} + +export function computeAdaptivePxPerDay (basePxPerDay: number, dataRangeWidth: number, viewportWidth: number): number { + if (basePxPerDay <= 0 || dataRangeWidth <= 0 || viewportWidth <= 0) return basePxPerDay + if (dataRangeWidth >= viewportWidth) return basePxPerDay + return basePxPerDay * (viewportWidth / dataRangeWidth) +} + +export function computeTickViewport ( + viewportLeft: number, + viewportRight: number, + dataRangeWidth: number, + overscan: number = 100 +): { left: number, right: number } { + const maxRight = Math.max(1, dataRangeWidth) + return { + left: Math.min(Math.max(0, viewportLeft - overscan), maxRight), + right: Math.min(Math.max(0, viewportRight + overscan), maxRight) + } +} + +/** + * Returns UTC-midnight timestamps for every non-working day in `[fromMs, toMs]`. + * Returns `[]` when `cfg` is undefined (legacy mode — no weekend tint to draw). + * + * The result is capped at `maxDays` entries as a safety net for unbounded + * viewport ranges; the default 366 covers a full year on screen, which is + * already beyond the supported zoom-out scenarios. + */ +export function nonWorkingDaysInRange ( + fromMs: number, + toMs: number, + cfg: WorkingCalendar | undefined, + maxDays: number = 366 +): number[] { + if (cfg === undefined) return [] + const startMs = Math.min(fromMs, toMs) + const endMs = Math.max(fromMs, toMs) + const start = Math.floor(startMs / DAY_MS) * DAY_MS + const end = Math.floor(endMs / DAY_MS) * DAY_MS + const out: number[] = [] + let cur = start + let safety = maxDays + while (cur <= end && safety-- > 0) { + if (!isWorkingDay(cur, cfg)) out.push(cur) + cur += DAY_MS + } + return out +} diff --git a/packages/gantt/src/working-days.ts b/packages/gantt/src/working-days.ts new file mode 100644 index 00000000000..72ca68a6716 --- /dev/null +++ b/packages/gantt/src/working-days.ts @@ -0,0 +1,160 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +// Convention: every timestamp reaching the Gantt is UTC-midnight-normalized at +// the data boundary (see `toGanttDay` in `time-scale.ts`, applied in the +// issue/milestone query callbacks of `GanttView.svelte`). The helpers here +// therefore assume — and re-assert via `utcMidnight` — that dates are UTC days, +// never local-midnight, keeping working-day/holiday classification and the +// scheduler/CPM in the same calendar frame regardless of the viewer's zone. + +import type { WorkingCalendar } from './types' + +const DAY_MS = 86_400_000 + +function utcMidnight (t: number): number { + return Math.floor(t / DAY_MS) * DAY_MS +} + +function holidayHas (cfg: WorkingCalendar, midnight: number): boolean { + // Linear scan — typical configs have ≤ 30 entries. Switch to Set if + // a very large holiday set would become slow; current usage is well below. + for (const h of cfg.holidays) { + if (utcMidnight(h) === midnight) return true + } + return false +} + +/** + * True iff `t` falls on a configured working day. Rounds `t` to UTC midnight + * before comparing weekday and holiday entries — so any time-of-day on a + * working date is still a working day. + */ +export function isWorkingDay (t: number, cfg: WorkingCalendar): boolean { + const midnight = utcMidnight(t) + if (holidayHas(cfg, midnight)) return false + const date = new Date(midnight) + // getUTCDay(): 0 = Sun, 1 = Mon, …, 6 = Sat + // weekdayMask bit 0 = Mon, …, bit 6 = Sun — remap accordingly. + const weekdayBit = (date.getUTCDay() + 6) % 7 + return (cfg.weekdayMask & (1 << weekdayBit)) !== 0 +} + +/** + * Returns the next working day ≥ `t` (UTC-midnight). If `t` itself is a + * working day, returns its midnight. Falls back to the input's midnight after + * 60 calendar-day iterations as a safety bail when no working days are + * configured (weekdayMask = 0 + no holidays granting any day). + */ +export function nextWorkingDay (t: number, cfg: WorkingCalendar): number { + let cur = utcMidnight(t) + for (let i = 0; i < 60; i++) { + if (isWorkingDay(cur, cfg)) return cur + cur += DAY_MS + } + return utcMidnight(t) +} + +/** + * Adds `n` working days to `t`. `n` may be negative (rewind). `n = 0` + * returns `t` unchanged (no auto-snap), so a user-pinned non-working date + * is preserved when no shift is requested. + * + * Safety: aborts after `|n| × 7 + 60` iterations to guard against + * non-progressing loops when all weekdays are non-working. + */ +export function addWorkingDays (t: number, n: number, cfg: WorkingCalendar): number { + if (n === 0) return t + const step = n > 0 ? DAY_MS : -DAY_MS + let remaining = Math.abs(n) + let cur = t + let safety = Math.abs(n) * 7 + 60 + while (remaining > 0 && safety-- > 0) { + cur += step + if (isWorkingDay(cur, cfg)) remaining-- + } + return cur +} + +/** + * Inclusive count of working days between `a` and `b` (both endpoints + * included if they themselves are working days). Returns a negative value + * when `a > b`, mirroring the semantics of `addWorkingDays(a, n)` where + * `n` would be negative. + * + * Used by critical-path slack rendering and by potential UI summaries. + */ +export function workingDaysBetween (a: number, b: number, cfg: WorkingCalendar): number { + const sign = a <= b ? 1 : -1 + const start = utcMidnight(Math.min(a, b)) + const end = utcMidnight(Math.max(a, b)) + let count = 0 + let cur = start + while (cur <= end) { + if (isWorkingDay(cur, cfg)) count++ + cur += DAY_MS + } + return count * sign +} + +/** + * Finish-to-Start anchor. + * + * Successor starts the working day *after* `predDue`, plus `lag` working + * days. `predDue` is the inclusive last day of the predecessor (project + * convention: both ends of a bar are inclusive days). + * + * Legacy mode (`cfg === undefined`) returns `predDue + (1 + lag) * DAY_MS`. + * The +1-day is intentional and matches critical-path.ts — this resolves + * the pre-Phase-2 off-by-one inconsistency where scheduler.ts forgot the + * +1-day (causing FS-anchored successors to be scheduled one day too early + * relative to CPM). + */ +export function fsAnchor (predDue: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return predDue + (1 + lag) * DAY_MS + return addWorkingDays(predDue, 1 + lag, cfg) +} + +/** Start-to-Start anchor: successor starts `lag` working days after `predStart`. */ +export function ssAnchor (predStart: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return predStart + lag * DAY_MS + return addWorkingDays(predStart, lag, cfg) +} + +/** Finish-to-Finish anchor: successor finishes `lag` working days after `predDue`. */ +export function ffAnchor (predDue: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return predDue + lag * DAY_MS + return addWorkingDays(predDue, lag, cfg) +} + +/** Start-to-Finish anchor: successor finishes `lag` working days after `predStart`. */ +export function sfAnchor (predStart: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return predStart + lag * DAY_MS + return addWorkingDays(predStart, lag, cfg) +} + +/** Inverse of {@link fsAnchor} for backward (pull-predecessor) traversal. */ +export function fsReverseAnchor (succStart: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return succStart - (1 + lag) * DAY_MS + return addWorkingDays(succStart, -(1 + lag), cfg) +} + +/** Inverse of {@link ssAnchor}. */ +export function ssReverseAnchor (succStart: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return succStart - lag * DAY_MS + return addWorkingDays(succStart, -lag, cfg) +} + +/** Inverse of {@link ffAnchor}. */ +export function ffReverseAnchor (succDue: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return succDue - lag * DAY_MS + return addWorkingDays(succDue, -lag, cfg) +} + +/** Inverse of {@link sfAnchor}. */ +export function sfReverseAnchor (succDue: number, lag: number, cfg: WorkingCalendar | undefined): number { + if (cfg === undefined) return succDue - lag * DAY_MS + return addWorkingDays(succDue, -lag, cfg) +} diff --git a/packages/gantt/src/y-viewport.ts b/packages/gantt/src/y-viewport.ts new file mode 100644 index 00000000000..f77256ff151 --- /dev/null +++ b/packages/gantt/src/y-viewport.ts @@ -0,0 +1,141 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Y-Viewport manager for the Gantt sidebar + canvas + arrow + * layer. Single source of truth: given the outer scroller's `scrollTop` + + * `viewportHeight` + the flattened row geometry, returns: + * + * - which row indices are visible (with `overscan` rows above + below), + * - the total scroll height (so the scroller knows how far it can scroll), + * - the `paddingTop` / `paddingBottom` spacer heights for the sidebar to + * keep the absolute-positioned visible rows aligned with their canvas + * counterparts at the same y. + * + * This module is the **pure-logic surface** that backs the on-screen + * virtualization. `@tanstack/svelte-virtual` is the + * runtime virtualizer for the on-screen render path — see + * `GanttSidebar.svelte` — but every coordinate decision the runtime + * virtualizer makes (visible-range slice, padding spacers, off-viewport + * arrow clipping, scroll-to-row) is mirrored here so we can test it + * deterministically without a browser. The sidebar grid + * `computeVisibleRowRange()` was the seed; this module is its v2. + */ + +/** Pixel-y bounds in canvas / sidebar coordinate space. */ +export interface YBounds { + /** Top of the viewport in pixels, inclusive. */ + top: number + /** Bottom of the viewport in pixels, exclusive. */ + bottom: number +} + +/** Input to {@link computeYViewport}. */ +export interface YViewportInput { + /** Total number of rows in the flattened layout. */ + rowCount: number + /** Uniform row height in pixels (Gantt v1 assumes a single height). */ + rowHeight: number + /** Current `scrollTop` of the outer scroller, in pixels. */ + scrollTop: number + /** Visible viewport height (the scroller's clientHeight), in pixels. */ + viewportHeight: number + /** Number of extra rows rendered above + below the visible range. Default 5. */ + overscan?: number +} + +/** Output of {@link computeYViewport}. */ +export interface YViewportSnapshot { + /** Inclusive `startIndex`, exclusive `endIndex` of the rendered slice. */ + visibleRange: { startIndex: number, endIndex: number } + /** Total scrollable height in pixels (`rowCount × rowHeight`). */ + totalSize: number + /** Spacer height above the first rendered row, in pixels. */ + paddingTop: number + /** Spacer height below the last rendered row, in pixels. */ + paddingBottom: number +} + +const DEFAULT_OVERSCAN = 5 + +/** + * Compute the visible row slice for a uniformly-sized list. Defensive for + * non-positive `rowHeight` (returns the full range) and negative scrollTop + * (rubber-band → clamp to 0). + */ +export function computeYViewport (input: YViewportInput): YViewportSnapshot { + const { rowCount, rowHeight, viewportHeight } = input + const overscan = input.overscan ?? DEFAULT_OVERSCAN + + if (rowCount <= 0) { + return { + visibleRange: { startIndex: 0, endIndex: 0 }, + totalSize: 0, + paddingTop: 0, + paddingBottom: 0 + } + } + + if (rowHeight <= 0 || !Number.isFinite(rowHeight)) { + return { + visibleRange: { startIndex: 0, endIndex: rowCount }, + totalSize: 0, + paddingTop: 0, + paddingBottom: 0 + } + } + + const scrollTop = Math.max(0, input.scrollTop) + const firstVisible = Math.floor(scrollTop / rowHeight) + const viewportRows = Math.max(0, Math.ceil(viewportHeight / rowHeight)) + const lastVisibleExclusive = firstVisible + viewportRows + + const startIndex = Math.max(0, firstVisible - Math.max(0, overscan)) + const endIndex = Math.min(rowCount, lastVisibleExclusive + Math.max(0, overscan)) + + const totalSize = rowCount * rowHeight + const paddingTop = startIndex * rowHeight + const paddingBottom = Math.max(0, (rowCount - endIndex) * rowHeight) + + return { + visibleRange: { startIndex, endIndex }, + totalSize, + paddingTop, + paddingBottom + } +} + +/** Pixel-y of a given row index. Clamps negative indices to 0. */ +export function rowIndexToY (index: number, rowHeight: number): number { + return Math.max(0, index) * rowHeight +} + +/** + * Pixel-y → row index (floor). Clamps to `[0, totalRows-1]`. Returns 0 for + * an empty list or non-positive `rowHeight` (defensive). + */ +export function yToRowIndex (y: number, rowHeight: number, totalRows: number): number { + if (totalRows <= 0 || rowHeight <= 0 || !Number.isFinite(rowHeight)) return 0 + if (y <= 0) return 0 + const idx = Math.floor(y / rowHeight) + return Math.min(totalRows - 1, idx) +} + +/** + * Pure variant of `filterVisibleRows` that works on the variable-height row + * layout used by the Gantt (issue + milestone + group-header). Returns the + * rows whose `[y, y + height)` intersects `bounds`. Used by the sidebar and + * arrow layer when the canvas already emits rows in their layout-y space. + */ +export function sliceVisibleRows (rows: readonly T[], bounds: YBounds): T[] { + if (rows.length === 0) return [] + const out: T[] = [] + for (const r of rows) { + if (r.y + r.height > bounds.top && r.y < bounds.bottom) { + out.push(r) + } + } + return out +} diff --git a/packages/gantt/src/zoom-dropdown.ts b/packages/gantt/src/zoom-dropdown.ts new file mode 100644 index 00000000000..d965149358e --- /dev/null +++ b/packages/gantt/src/zoom-dropdown.ts @@ -0,0 +1,94 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { ZoomLevel } from './types' +import { MIN_PPD, ZOOM_PX_PER_DAY } from './zoom' + +/** + * Dropdown selection model for the Gantt toolbar. + * + * The toolbar's four legacy preset buttons collapse into a single + * Dropdown + a numeric "X days" input. Selection is one of five tags: + * + * day | week | month | quarter | custom + * + * `custom` is **not** directly user-selectable from the dropdown — it is + * shown only when `userPxPerDay` does not match any of the four preset + * px/day values within `EPSILON_PPD`. Wheel-zoom flips selection to + * `custom` automatically; clicking a preset clears `userPxPerDay = null`. + */ + +export type DropdownSelection = ZoomLevel | 'custom' + +/** + * Tolerance for matching a continuous px/day value back to a preset. + * + * Continuous wheel-zoom can produce values like 31.997 that should still + * highlight "Day". 0.05 px/day is well below the smallest preset (1.5) + * and below any wheel-step that ever yields a visible move. + */ +export const EPSILON_PPD = 0.05 + +export const MIN_VISIBLE_DAYS = 1 +export const MAX_VISIBLE_DAYS = 999 + +/** + * Decide which dropdown row should appear "selected" given the current + * `userPxPerDay` override and the active `zoom` preset. + * + * - `userPxPerDay === null` → the preset button is active → return `zoom`. + * - Else if `userPxPerDay` is within `EPSILON_PPD` of any preset, return + * that preset (the user wheeled exactly back onto a preset). + * - Else return `'custom'`. + */ +export function dropdownSelectionForPxPerDay (userPxPerDay: number | null, zoom: ZoomLevel): DropdownSelection { + if (userPxPerDay === null) return zoom + if (!Number.isFinite(userPxPerDay) || userPxPerDay <= 0) return 'custom' + const levels: ZoomLevel[] = ['day', 'week', 'month', 'quarter'] + for (const lvl of levels) { + if (Math.abs(userPxPerDay - ZOOM_PX_PER_DAY[lvl]) <= EPSILON_PPD) return lvl + } + return 'custom' +} + +/** + * How many days currently fit into the horizontal scroller viewport at + * the given px/day scale. Used for the read-out / editable "X days" + * input next to the Dropdown. + * + * Always returns at least `MIN_VISIBLE_DAYS` (1) and defends against + * NaN, zero or negative inputs so the UI never displays nonsense. + */ +export function visibleDaysFromPxPerDay (viewportWidth: number, pxPerDay: number): number { + if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) return MIN_VISIBLE_DAYS + if (!Number.isFinite(pxPerDay) || pxPerDay <= 0) return MIN_VISIBLE_DAYS + const raw = Math.round(viewportWidth / pxPerDay) + if (raw < MIN_VISIBLE_DAYS) return MIN_VISIBLE_DAYS + return raw +} + +/** + * Inverse of `visibleDaysFromPxPerDay`: given a user-typed day count and + * the current viewport width, compute the px/day that would make exactly + * that many days fit. Used when the user edits the "X days" input. + * + * - `days` is clamped to `[MIN_VISIBLE_DAYS, MAX_VISIBLE_DAYS]`. + * - Returns `MIN_PPD` for invalid viewport widths so the caller can + * still apply the result without further error handling. + */ +export function pxPerDayFromVisibleDays (viewportWidth: number, days: number): number { + if (!Number.isFinite(viewportWidth) || viewportWidth <= 0) return MIN_PPD + let clamped: number + if (!Number.isFinite(days)) { + clamped = MIN_VISIBLE_DAYS + } else if (days < MIN_VISIBLE_DAYS) { + clamped = MIN_VISIBLE_DAYS + } else if (days > MAX_VISIBLE_DAYS) { + clamped = MAX_VISIBLE_DAYS + } else { + clamped = days + } + return viewportWidth / clamped +} diff --git a/packages/gantt/src/zoom.ts b/packages/gantt/src/zoom.ts new file mode 100644 index 00000000000..243b908f1ea --- /dev/null +++ b/packages/gantt/src/zoom.ts @@ -0,0 +1,127 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { ZoomLevel } from './types' + +/** + * C — continuous (Ctrl+Wheel) zoom math. + * + * The 4 toolbar buttons (Day/Week/Month/Quarter) remain the entry-points + * for "snap to a sensible preset". On top of that, Ctrl+Wheel (Cmd+Wheel + * on Mac) provides a continuous pixelsPerDay knob. We keep `zoom` as the + * source of truth for header tick-granularity, but allow a numeric + * override (`pxPerDayOverride`) to drive the actual horizontal scale. + * + * The preset values mirror the legacy `PX_PER_DAY` table in time-scale.ts. + * `presetForPxPerDay` picks the closest preset (used by the toolbar + * highlight after a wheel-zoom). + */ + +export const MIN_PPD = 0.05 +export const MAX_PPD = 200 + +/** Px-per-day preset for each ZoomLevel — matches time-scale.ts. */ +export const ZOOM_PX_PER_DAY: Record = { + day: 32, + week: 14, + month: 4, + quarter: 1.5 +} + +/** Clamp px/day to the supported continuous range. */ +export function clampPxPerDay (ppd: number): number { + if (Number.isNaN(ppd)) return MIN_PPD + if (ppd <= 0) return MIN_PPD + if (ppd < MIN_PPD) return MIN_PPD + if (ppd > MAX_PPD) return MAX_PPD + return ppd +} + +/** + * Adaptive sensitivity for `applyWheelZoom`. Below ~4 px/day (month and + * quarter presets) a uniform factor produces visibly slower zoom because + * the same per-notch multiplicative step traverses a smaller absolute + * pixel range. Bump the factor for the low-density bands so users feel + * the same responsiveness at every zoom level. + */ +export function adaptiveWheelFactor (currentPpd: number): number { + if (!Number.isFinite(currentPpd) || currentPpd <= 0) return 0.012 + if (currentPpd < 4) return 0.012 + return 0.006 +} + +/** + * Apply a single wheel-step (typically `event.deltaY`) to a current + * px-per-day value. Uses an exponential mapping so that scrolling N + * notches in the same direction zooms by the same factor regardless of + * starting scale (no linear "snap once you cross zero" feel). Result is + * clamped to [MIN_PPD, MAX_PPD]. + * + * Positive `deltaY` (wheel down) zooms out; negative zooms in — matching + * the convention used by browsers, Figma, MS Project, and Asana. + * + * When `factor` is omitted the per-density adaptive value from + * `adaptiveWheelFactor` is used. Callers that need a fixed sensitivity + * (e.g. tests) can pass an explicit factor. + */ +export function applyWheelZoom (currentPpd: number, deltaY: number, factor?: number): number { + if (!Number.isFinite(currentPpd) || currentPpd <= 0) return clampPxPerDay(MIN_PPD) + if (!Number.isFinite(deltaY) || deltaY === 0) return clampPxPerDay(currentPpd) + const f = factor ?? adaptiveWheelFactor(currentPpd) + const next = currentPpd * Math.exp(-deltaY * f) + return clampPxPerDay(next) +} + +/** + * Compute the new scrollLeft so that the date currently under the cursor + * stays anchored under the cursor after a zoom change. The math is purely + * pixel-based and stateless — caller passes in the cursor position + * relative to the scroller, the current scrollLeft, and the old/new + * px-per-day values. Origin is implicit (cancels out). + */ +export function cursorAnchoredScrollLeft ( + cursorX: number, + oldScrollLeft: number, + oldPpd: number, + newPpd: number +): number { + if (!Number.isFinite(oldPpd) || oldPpd <= 0) return oldScrollLeft + if (!Number.isFinite(newPpd) || newPpd <= 0) return oldScrollLeft + const ratio = newPpd / oldPpd + // x_world(old) = (oldScrollLeft + cursorX) / oldPpd (in "days" units) + // We want x_world(new) === x_world(old), so: + // (newScrollLeft + cursorX) / newPpd = (oldScrollLeft + cursorX) / oldPpd + // newScrollLeft = ratio * (oldScrollLeft + cursorX) - cursorX + const next = ratio * (oldScrollLeft + cursorX) - cursorX + return Math.max(0, next) +} + +/** + * Adaptive tick-granularity selection based on the current px-per-day. + * Thresholds were chosen so each preset's natural pxPerDay falls into + * the matching band, and so two adjacent presets share a sensible + * boundary roughly mid-way between them on a log scale: + * + * day=32 → day-ticks at > 20 + * week=14 → week-ticks in (7 .. 20] + * month=4 → month-ticks in (2 .. 7] + * quarter=1.5 → quarter-ticks at <= 2 + * + * Returning a `ZoomLevel` keeps backward-compat with the existing + * `createTimeScale(zoom, origin, pxPerDayOverride)` API: callers don't + * need a new tick generator. + */ +export function pxPerDayToTickZoom (ppd: number): ZoomLevel { + if (!Number.isFinite(ppd) || ppd <= 0) return 'quarter' + if (ppd > 20) return 'day' + if (ppd > 7) return 'week' + if (ppd > 2) return 'month' + return 'quarter' +} + +/** Inverse of `pxPerDayToTickZoom` for the toolbar "active preset" highlight. */ +export function presetForPxPerDay (ppd: number): ZoomLevel { + return pxPerDayToTickZoom(ppd) +} diff --git a/packages/gantt/tsconfig.json b/packages/gantt/tsconfig.json new file mode 100644 index 00000000000..7d78e05abb4 --- /dev/null +++ b/packages/gantt/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} diff --git a/packages/importer/src/huly/huly.ts b/packages/importer/src/huly/huly.ts index 9a272711e6c..b4ddfd1be62 100644 --- a/packages/importer/src/huly/huly.ts +++ b/packages/importer/src/huly/huly.ts @@ -75,6 +75,24 @@ export interface HulyIssueHeader { estimation?: number // in hours remainingTime?: number // in hours comments?: HulyComment[] + // Schedule fields surfaced in the Gantt viewlet. + // Dates are ISO YYYY-MM-DD; the importer treats them as UTC midnight. + startDate?: string + dueDate?: string + // Independent soft deadline. Renders a flag marker in + // the Gantt; turns red+pulsing when dueDate > deadline. + deadline?: string + // Foreign-key references to Component / Milestone by their human + // label. Looked up by label at build time against the components/ + // milestones declared on the parent Project's YAML. Unknown labels + // are reported as an importer error. + component?: string + milestone?: string + // Finish-to-Start dependency list, by predecessor issue + // identifier (e.g. 'GAME-3') or by relative front-matter title. + // Resolved at build time after all issue identifiers are assigned. + // Limited to FS kind for the first cut. + predecessors?: string[] } export interface HulySpaceSettings { @@ -89,12 +107,33 @@ export interface HulySpaceSettings { emoji?: string } +export interface HulyProjectComponent { + label: string + description?: string +} + +export interface HulyProjectMilestone { + label: string + description?: string + // ISO date YYYY-MM-DD (UTC midnight) + targetDate: string + // Optional ISO start date + startDate?: string +} + export interface HulyProjectSettings extends HulySpaceSettings { class: 'tracker:class:Project' identifier: string id?: 'tracker:project:DefaultProject' projectType?: string defaultIssueStatus?: string + // Declarative components + milestones on the project. + // Issues reference them by label (case-sensitive) via their + // `component` / `milestone` front-matter fields. Each entry is + // materialised as a single tracker.class.Component / Milestone doc + // in the project's space. + components?: HulyProjectComponent[] + milestones?: HulyProjectMilestone[] } export interface HulyTeamspaceSettings extends HulySpaceSettings { @@ -404,6 +443,41 @@ export class HulyFormatImporter { this.metadataRegistry.setRefMetadata(issuePath, tracker.class.Issue, `${projectIdentifier}-${issueNumber}`) + const parseIsoDate = (iso?: string): number | undefined => { + if (iso === undefined || iso === null || String(iso).trim() === '') return undefined + const s = String(iso).trim() + // Relative-date placeholders for the Game Design + // Example and similar demo workspaces. Forms supported: + // today — UTC midnight of install day + // today+5d — N days after install + // today-3d — N days before install + // today+2w — N weeks + // today+1mo — N calendar months + // + // Note: bare `today...` syntax (not `${today...}`) so the + // global UnifiedFormatParser doesn't try to resolve it as + // a script-level variable and throw 'Variable not found'. + const relMatch = s.match(/^today(?:([+-])(\d+)(d|w|mo))?$/) + if (relMatch !== null) { + const today = new Date() + today.setUTCHours(0, 0, 0, 0) + if (relMatch[1] === undefined) return today.getTime() + const sign = relMatch[1] === '+' ? 1 : -1 + const n = Number(relMatch[2]) * sign + if (relMatch[3] === 'd') return today.getTime() + n * 86_400_000 + if (relMatch[3] === 'w') return today.getTime() + n * 7 * 86_400_000 + if (relMatch[3] === 'mo') { + const r = new Date(today.getTime()) + r.setUTCMonth(r.getUTCMonth() + n) + return r.getTime() + } + return today.getTime() + } + const [y, m, d] = s.split('-').map(Number) + if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return undefined + return Date.UTC(y, m - 1, d) + } + const issue: ImportIssue = { id: this.metadataRegistry.getRef(issuePath) as Ref, class: tracker.class.Issue, @@ -416,7 +490,14 @@ export class HulyFormatImporter { remainingTime: issueHeader.remainingTime, comments: await this.processComments(currentPath, issueHeader.comments), subdocs: [], // Will be added via builder - assignee: this.findPersonByName(issueHeader.assignee) + assignee: this.findPersonByName(issueHeader.assignee), + // Gantt scheduling fields + startDate: parseIsoDate(issueHeader.startDate), + dueDate: parseIsoDate(issueHeader.dueDate), + deadline: parseIsoDate(issueHeader.deadline), + componentLabel: issueHeader.component, + milestoneLabel: issueHeader.milestone, + predecessors: issueHeader.predecessors } builder.addIssue(projectPath, issuePath, issue, parentIssuePath) @@ -613,6 +694,29 @@ export class HulyFormatImporter { } private async processProject (data: HulyProjectSettings): Promise { + const parseIsoDate = (iso?: string): number | undefined => { + if (iso === undefined || iso === null || String(iso).trim() === '') return undefined + const s = String(iso).trim() + const relMatch = s.match(/^today(?:([+-])(\d+)(d|w|mo))?$/) + if (relMatch !== null) { + const today = new Date() + today.setUTCHours(0, 0, 0, 0) + if (relMatch[1] === undefined) return today.getTime() + const sign = relMatch[1] === '+' ? 1 : -1 + const n = Number(relMatch[2]) * sign + if (relMatch[3] === 'd') return today.getTime() + n * 86_400_000 + if (relMatch[3] === 'w') return today.getTime() + n * 7 * 86_400_000 + if (relMatch[3] === 'mo') { + const r = new Date(today.getTime()) + r.setUTCMonth(r.getUTCMonth() + n) + return r.getTime() + } + return today.getTime() + } + const [y, m, d] = s.split('-').map(Number) + if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return undefined + return Date.UTC(y, m - 1, d) + } return { class: tracker.class.Project, id: data.id as Ref, @@ -626,7 +730,15 @@ export class HulyFormatImporter { defaultIssueStatus: data.defaultIssueStatus !== undefined ? { name: data.defaultIssueStatus } : undefined, owners: data.owners !== undefined ? data.owners.map((name) => this.findAccountByName(name)) : [], members: data.members !== undefined ? data.members.map((name) => this.findAccountByName(name)) : [], - docs: [] + docs: [], + // Declarative components + milestones + components: data.components, + milestones: data.milestones?.map((m) => ({ + label: m.label, + description: m.description, + targetDate: parseIsoDate(m.targetDate) ?? Date.now(), + startDate: parseIsoDate(m.startDate) + })) } } diff --git a/packages/importer/src/importer/importer.ts b/packages/importer/src/importer/importer.ts index 5c5d30b7b5b..3d3562edb03 100644 --- a/packages/importer/src/importer/importer.ts +++ b/packages/importer/src/importer/importer.ts @@ -140,6 +140,24 @@ export interface ImportProject extends ImportSpace { projectType?: ImportProjectType defaultIssueStatus?: ImportStatus description?: string + // Declarative components + milestones associated with + // this project. The importer materialises one Component / Milestone + // doc per entry and exposes the labels to ImportIssue look-ups. + components?: ImportProjectComponent[] + milestones?: ImportProjectMilestone[] +} + +export interface ImportProjectComponent { + label: string + description?: string +} + +export interface ImportProjectMilestone { + label: string + description?: string + // UTC midnight epoch ms; HulyFormatImporter parses YYYY-MM-DD inputs. + targetDate: number + startDate?: number } export interface ImportIssue extends ImportDoc { @@ -152,6 +170,23 @@ export interface ImportIssue extends ImportDoc { estimation?: number remainingTime?: number comments?: ImportComment[] + // Gantt-scheduling fields. Dates are UTC midnight epoch + // ms; HulyFormatImporter parses YYYY-MM-DD inputs. + startDate?: number + dueDate?: number + // Soft deadline; renders a flag marker in the Gantt. + deadline?: number + // Component / Milestone references — by LABEL at parse time, the + // builder resolves them to actual Ref / Ref + // against the project's `components` / `milestones` arrays at + // build time. Unknown labels cause a validation error. + componentLabel?: string + milestoneLabel?: string + // Finish-to-start dependency list. Each entry is a + // FRONT-MATTER issue title or an absolute identifier (e.g. + // 'GAME-3'). The builder resolves the references to Ref + // and emits one IssueRelation per pair after all issues exist. + predecessors?: string[] } export interface ImportComment { @@ -434,12 +469,192 @@ export class WorkspaceImporter { throw new Error('Project not found: ' + projectId) } + // Create Component / Milestone docs declared on the + // project. The maps keep them addressable by label for later + // resolution from issue front-matter. + const componentIds = new Map>() + const milestoneIds = new Map>() + if (project.components !== undefined) { + for (const c of project.components) { + const id = generateId() + await this.client.createDoc( + tracker.class.Component, + projectId, + { + label: c.label, + description: c.description ?? '', + lead: null, + comments: 0, + attachments: 0 + }, + id + ) + componentIds.set(c.label, id) + } + } + if (project.milestones !== undefined) { + for (const m of project.milestones) { + const id = generateId() + await this.client.createDoc( + tracker.class.Milestone, + projectId, + { + label: m.label, + description: m.description ?? '', + status: 0, // MilestoneStatus.Planned + comments: 0, + attachments: 0, + startDate: m.startDate ?? null, + targetDate: m.targetDate + }, + id + ) + milestoneIds.set(m.label, id) + } + } + + // Issue id ↔ identifier index for predecessor resolution after + // all issues are created. + const issueIdByTitle = new Map>() + const issueIdByIdentifier = new Map>() + for (const issue of project.docs) { - await this.createIssueWithSubissues(issue, tracker.ids.NoParent, projectDoc, projectId, []) + await this.createIssueWithSubissuesEx( + issue, + tracker.ids.NoParent, + projectDoc, + projectId, + [], + componentIds, + milestoneIds, + issueIdByTitle, + issueIdByIdentifier + ) + } + + // Resolve predecessors → IssueRelation docs after all issues exist. + const collectAllIssues = (docs: ImportIssue[], out: ImportIssue[] = []): ImportIssue[] => { + for (const d of docs) { + out.push(d) + if (d.subdocs?.length > 0) collectAllIssues(d.subdocs as ImportIssue[], out) + } + return out + } + const allIssues = collectAllIssues(project.docs) + for (const issue of allIssues) { + if (issue.predecessors === undefined || issue.predecessors.length === 0) continue + const targetId = issueIdByTitle.get(issue.title) ?? issue.id + if (targetId === undefined) { + this.logger.error(`Cannot resolve target for predecessors of "${issue.title}"`) + continue + } + for (const predRef of issue.predecessors) { + const fromId = issueIdByIdentifier.get(predRef) ?? issueIdByTitle.get(predRef) + if (fromId === undefined) { + this.logger.error(`Unknown predecessor "${predRef}" for issue "${issue.title}"`) + continue + } + await this.client.addCollection( + tracker.class.IssueRelation, + projectId, + fromId, + tracker.class.Issue, + 'relations', + { target: targetId, kind: 'finish-to-start', lag: 0 } + ) + } } return projectId } + async createIssueWithSubissuesEx ( + issue: ImportIssue, + parentId: Ref, + project: Project, + spaceId: Ref, + parentsInfo: IssueParentInfo[], + componentIds: Map>, + milestoneIds: Map>, + issueIdByTitle: Map>, + issueIdByIdentifier: Map> + ): Promise<{ id: Ref, identifier: string }> { + this.logger.log('Creating issue: ' + issue.title) + const issueResult = await this.createIssueWithSchedule( + issue, + project, + parentId, + spaceId, + parentsInfo, + componentIds, + milestoneIds + ) + issueIdByTitle.set(issue.title, issueResult.id) + issueIdByIdentifier.set(issueResult.identifier, issueResult.id) + + if (issue.subdocs?.length > 0) { + const parentsInfoEx = [ + { + parentId: issueResult.id, + parentTitle: issue.title, + space: spaceId, + identifier: issueResult.identifier + }, + ...parentsInfo + ] + + for (const child of issue.subdocs) { + await this.createIssueWithSubissuesEx( + child as ImportIssue, + issueResult.id, + project, + spaceId, + parentsInfoEx, + componentIds, + milestoneIds, + issueIdByTitle, + issueIdByIdentifier + ) + } + } + + return issueResult + } + + async createIssueWithSchedule ( + issue: ImportIssue, + project: Project, + parentId: Ref, + spaceId: Ref, + parentsInfo: IssueParentInfo[], + componentIds: Map>, + milestoneIds: Map> + ): Promise<{ id: Ref, identifier: string }> { + // Delegate to the original createIssue path, then update the + // scheduling-related fields if the front-matter declared them. + // We do it in two steps to keep `createIssue` backward-compatible + // for callers (Linear/Notion/Trello importers) that don't pass + // the new component/milestone maps. + const base = await this.createIssue(issue, project, parentId, spaceId, parentsInfo) + const patch: Partial = {} + if (issue.startDate !== undefined) patch.startDate = issue.startDate + if (issue.dueDate !== undefined) patch.dueDate = issue.dueDate + if (issue.deadline !== undefined) patch.deadline = issue.deadline + if (issue.componentLabel !== undefined) { + const cid = componentIds.get(issue.componentLabel) + if (cid !== undefined) patch.component = cid + else this.logger.error(`Unknown component "${issue.componentLabel}" on issue "${issue.title}"`) + } + if (issue.milestoneLabel !== undefined) { + const mid = milestoneIds.get(issue.milestoneLabel) + if (mid !== undefined) patch.milestone = mid + else this.logger.error(`Unknown milestone "${issue.milestoneLabel}" on issue "${issue.title}"`) + } + if (Object.keys(patch).length > 0) { + await this.client.updateDoc(tracker.class.Issue, spaceId, base.id, patch) + } + return base + } + async createIssueWithSubissues ( issue: ImportIssue, parentId: Ref, diff --git a/packages/presentation/src/components/DocPopup.svelte b/packages/presentation/src/components/DocPopup.svelte index 9568beeb814..fa09225a2eb 100644 --- a/packages/presentation/src/components/DocPopup.svelte +++ b/packages/presentation/src/components/DocPopup.svelte @@ -52,7 +52,7 @@ export let placeholder: IntlString = presentation.string.Search export let selectedObjects: Ref[] = [] export let shadows: boolean = true - export let width: 'medium' | 'large' | 'full' | 'auto' = 'medium' + export let width: 'medium' | 'large' | 'full' | 'auto' | 'resizable' = 'medium' export let size: 'small' | 'medium' | 'large' = 'large' export let noSearchField: boolean = false @@ -222,6 +222,7 @@ class:full-width={width === 'full'} class:plainContainer={!shadows} class:width-40={width === 'large'} + class:width-resizable={width === 'resizable'} class:auto={width === 'auto'} class:embedded on:keydown={onKeydown} diff --git a/packages/presentation/src/components/ObjectPopup.svelte b/packages/presentation/src/components/ObjectPopup.svelte index 3b467a6813e..5e651b01760 100644 --- a/packages/presentation/src/components/ObjectPopup.svelte +++ b/packages/presentation/src/components/ObjectPopup.svelte @@ -43,7 +43,7 @@ export let selectedObjects: Ref[] = [] export let ignoreObjects: Ref[] = [] export let shadows: boolean = true - export let width: 'medium' | 'large' | 'full' | 'auto' = 'medium' + export let width: 'medium' | 'large' | 'full' | 'auto' | 'resizable' = 'medium' export let size: 'small' | 'medium' | 'large' = 'large' export let searchMode: 'field' | 'fulltext' | 'disabled' | 'spotlight' = 'field' diff --git a/packages/theme/styles/components.scss b/packages/theme/styles/components.scss index abfa53a5e71..e065a01c423 100644 --- a/packages/theme/styles/components.scss +++ b/packages/theme/styles/components.scss @@ -518,6 +518,11 @@ margin: 0 -.25rem 0 1rem; padding: .25rem; } + /* Paired with `.search.shrink` below: when the consumer designates the + search group as the row's shrink target, the extra group must keep + its natural width — otherwise both groups shrink proportionally and + the trailing buttons get clipped anyway. */ + &.freeze { flex-shrink: 0; } } &.before { gap: var(--spacing-0_5); @@ -544,6 +549,19 @@ .hulyHeader-row { min-height: var(--spacing-6_5); } .hulyHeader-buttonsGroup.search { flex-direction: row-reverse; } + /* Opt-in via
. `flex: 1 1 0` makes the group's used + width a pure function of the row width minus the frozen siblings, i.e. + independent of its own content — which is what lets a consumer measure + the space it got and collapse its controls without a feedback loop. + `justify-content: flex-end` is "pack towards main-end", and main-end in + this row-reverse group is the LEFT edge, so the content keeps sitting + where it did before instead of drifting to the right. */ + .hulyHeader-buttonsGroup.search.shrink { + flex: 1 1 0; + min-width: 0; + justify-content: flex-end; + overflow: hidden; + } .hulyHeader-buttonsGroup.actions { margin-left: 1rem; } } &:not(.doubleRow) { diff --git a/packages/theme/styles/popups.scss b/packages/theme/styles/popups.scss index 7f94e7c7fb8..fcc0b05bddd 100644 --- a/packages/theme/styles/popups.scss +++ b/packages/theme/styles/popups.scss @@ -168,7 +168,17 @@ width: 100%; max-width: 100%; } - + &.width-resizable { + width: 48rem; + max-width: min(96vw, 80rem); + min-width: 28rem; + height: 36rem; + max-height: min(96vh, 60rem); + min-height: 16rem; + resize: both; + overflow: hidden; + } + &.maxHeight { height: 22rem; } &.autoHeight { max-height: calc(100vh - 2rem); diff --git a/packages/ui/src/components/Header.svelte b/packages/ui/src/components/Header.svelte index 8dc2348f20c..c58ca2d26c6 100644 --- a/packages/ui/src/components/Header.svelte +++ b/packages/ui/src/components/Header.svelte @@ -37,6 +37,24 @@ export let hideExtra: boolean = false export let hidePresence: boolean = false export let overflowExtra: boolean = false + /** + * Opt-in: make the `search` group the row's designated shrink target and + * freeze the `extra` group. + * + * By default every `.hulyHeader-buttonsGroup` except `extra` is + * `flex-shrink: 0`, so a consumer that puts a wide control cluster into the + * `search` slot (the Tracker's Gantt toolbar) grew the row past its own + * `overflow: hidden` box: `extra` was squeezed to a few pixels and the + * buttons inside it ended up outside the viewport — in the DOM, but + * unclickable. With this flag the roles are swapped: `search` shrinks (and + * is expected to collapse its own content), `extra` keeps its natural + * width. Default `false`, so no existing consumer changes behaviour. + * + * Only honoured in the double-row layout; the single-row layout puts + * `search` between a growing title group and `actions`, where a second + * flexible item would fight the title for space. + */ + export let shrinkSearch: boolean = false export let noPrint: boolean = false export let freezeBefore: boolean = false export let doubleRowWidth: number = 768 @@ -168,7 +186,7 @@ {/if} {#if $$slots.search} -