diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 16a9bf5..4dd9295 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -39,3 +39,10 @@ updates: patterns: ["*"] cooldown: default-days: 3 + + - package-ecosystem: "npm" + directory: "/e2e" + multi-ecosystem-group: "dependencies" + patterns: ["*"] + cooldown: + default-days: 3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fa6b1f..d413303 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,7 @@ jobs: run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... - name: Upload coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: use_oidc: true fail_ci_if_error: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 38454f4..558dea3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,7 +44,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -72,6 +72,6 @@ jobs: CGO_ENABLED: 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c3be35a..7b97ccd 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -33,7 +33,7 @@ jobs: working-directory: ./e2e - name: Install Playwright Browsers - run: pnpx playwright install --with-deps chromium + run: pnpm exec playwright install --with-deps chromium working-directory: ./e2e - name: Run E2E tests diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c743e7d..368b994 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,4 +19,4 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 diff --git a/agent.Dockerfile b/agent.Dockerfile index 2ebbab3..eb8de69 100644 --- a/agent.Dockerfile +++ b/agent.Dockerfile @@ -1,6 +1,6 @@ FROM bufbuild/buf:1.69 AS buf -FROM golang:1.26-trixie AS builder +FROM golang:1.26.3-trixie AS builder ARG VERSION=dev ARG COMMIT=none diff --git a/backend/go.mod b/backend/go.mod index ce3661f..b9f6ae5 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,7 +7,7 @@ require ( github.com/aegis-aead/go-libaegis v0.2.14 github.com/alexedwards/argon2id v1.0.1-0.20251028180742-493d7dead70e github.com/coreos/go-oidc/v3 v3.18.0 - github.com/docker/cli v29.4.3+incompatible + github.com/docker/cli v29.5.1+incompatible github.com/docker/compose/v5 v5.1.3 github.com/gin-gonic/gin v1.12.0 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -15,6 +15,7 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/moby/moby/api v1.54.2 github.com/moby/moby/client v0.4.1 + github.com/nicholas-fedor/shoutrrr v0.15.0 github.com/rs/zerolog v1.35.1 github.com/spf13/cobra v1.10.2 golang.org/x/oauth2 v0.36.0 @@ -34,6 +35,7 @@ require ( github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/eclipse/paho.golang v0.23.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/in-toto/attestation v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect @@ -106,7 +108,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mattn/go-sqlite3 v1.14.37 // indirect @@ -129,7 +131,7 @@ require ( github.com/morikuni/aec v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/quic-go/qpack v0.6.0 // indirect @@ -170,12 +172,12 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/arch v0.25.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 7e4b264..8cb40e2 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,6 +8,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= @@ -118,8 +120,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/buildx v0.33.0 h1:xuZeuQe/C/2tvLDgiIA6+Ynq3FFWSfsGNWIHM3q1hD8= github.com/docker/buildx v0.33.0/go.mod h1:7JVma62htERKE5iy5YD1q64PKiAHUzXuhSBd4oq3I74= -github.com/docker/cli v29.4.3+incompatible h1:u+UliYm2J/rYrIh2FqHQg32neRG8GjbvNuwQRTzGspU= -github.com/docker/cli v29.4.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs= +github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/compose/v5 v5.1.3 h1:Pe8JKGKnL/9xVvuflKmtlCR6AfPTGbML/PbpMJA+Gks= github.com/docker/compose/v5 v5.1.3/go.mod h1:5WS4y+TCdaA6unNMIuVbp7SbMZ1m6KduTAyqAtD8vz8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -134,6 +136,8 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvSyR6Mk= +github.com/eclipse/paho.golang v0.23.0/go.mod h1:nQRhTkoZv8EAiNs5UU0/WdQIx2NrnWUpL9nsGJTQN04= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -205,6 +209,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= @@ -230,6 +236,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -257,6 +265,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= +github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -283,8 +293,8 @@ github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= @@ -339,9 +349,16 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nicholas-fedor/shoutrrr v0.15.0 h1:4gKIev9ucsY50dy+GkkPQKyfIJdKOEwr0dnsVpGdDWk= +github.com/nicholas-fedor/shoutrrr v0.15.0/go.mod h1:Z6b9KNn8q9nXl27/p39zo7iJHBmqkigzVxWnbfwcU8w= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -352,8 +369,8 @@ github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22 github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= +github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -413,6 +430,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -500,20 +519,20 @@ golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -528,21 +547,22 @@ golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/backend/internal/hub/applications/applications_test.go b/backend/internal/hub/applications/applications_test.go index 13c5935..4c153fa 100644 --- a/backend/internal/hub/applications/applications_test.go +++ b/backend/internal/hub/applications/applications_test.go @@ -341,6 +341,7 @@ func TestNewQueue_HasCorrectProperties(t *testing.T) { q := NewQueue(&nop) if q == nil { t.Fatal("expected non-nil queue") + return } if q.workers != defaultWorkerCount { t.Errorf("expected %d workers, got %d", defaultWorkerCount, q.workers) diff --git a/backend/internal/hub/applications/poller_test.go b/backend/internal/hub/applications/poller_test.go index 54c2f7d..5450556 100644 --- a/backend/internal/hub/applications/poller_test.go +++ b/backend/internal/hub/applications/poller_test.go @@ -87,6 +87,7 @@ func TestNewPoller_HasCorrectProperties(t *testing.T) { p := NewPoller(&nop) if p == nil { t.Fatal("expected non-nil poller") + return } if p.done == nil { t.Error("expected non-nil done channel") diff --git a/backend/internal/hub/auth/cookie_test.go b/backend/internal/hub/auth/cookie_test.go index eeeb7f1..08a5843 100644 --- a/backend/internal/hub/auth/cookie_test.go +++ b/backend/internal/hub/auth/cookie_test.go @@ -48,6 +48,7 @@ func TestSetAuthCookie(t *testing.T) { cookie := findCookie(w.Result().Cookies(), defaultCookieConfig.Name) if cookie == nil { t.Fatalf("cookie %q not set in response", defaultCookieConfig.Name) + return } if cookie.Value != "test-token" { t.Errorf("Value = %q, want %q", cookie.Value, "test-token") @@ -70,6 +71,7 @@ func TestClearAuthCookie(t *testing.T) { cookie := findCookie(w.Result().Cookies(), defaultCookieConfig.Name) if cookie == nil { t.Fatalf("cookie %q not set in response", defaultCookieConfig.Name) + return } if cookie.Value != "" { t.Errorf("Value = %q, want empty", cookie.Value) diff --git a/backend/internal/hub/db/db_test.go b/backend/internal/hub/db/db_test.go index cbecde8..6792a08 100644 --- a/backend/internal/hub/db/db_test.go +++ b/backend/internal/hub/db/db_test.go @@ -63,6 +63,8 @@ func TestRunMigrations_AllTablesExist(t *testing.T) { "user_oidc_identities", "repositories", "applications", + "notifications", + "application_notifications", } for _, table := range tables { if !db.Migrator().HasTable(table) { @@ -248,6 +250,50 @@ func TestRunMigrations_ApplicationsTableSchema(t *testing.T) { } } +func TestRunMigrations_NotificationsTableSchema(t *testing.T) { + gormDB := openTestDB(t) + if err := runMigrations(gormDB); err != nil { + t.Fatalf("runMigrations() error: %v", err) + } + + sqlDB, err := gormDB.DB() + if err != nil { + t.Fatalf("failed to get sql.DB: %v", err) + } + cols := columnNames(t, sqlDB, "notifications") + + required := []string{ + "id", "name", "enabled", "enable_by_default", + "status", "type", "config", + "created_at", "updated_at", + } + for _, col := range required { + if !cols[col] { + t.Errorf("notifications table missing column %q", col) + } + } +} + +func TestRunMigrations_ApplicationNotificationsTableSchema(t *testing.T) { + gormDB := openTestDB(t) + if err := runMigrations(gormDB); err != nil { + t.Fatalf("runMigrations() error: %v", err) + } + + sqlDB, err := gormDB.DB() + if err != nil { + t.Fatalf("failed to get sql.DB: %v", err) + } + cols := columnNames(t, sqlDB, "application_notifications") + + required := []string{"application_id", "notification_id"} + for _, col := range required { + if !cols[col] { + t.Errorf("application_notifications table missing column %q", col) + } + } +} + func TestRunMigrations_UserOIDCIdentitiesIndexes(t *testing.T) { gormDB := openTestDB(t) if err := runMigrations(gormDB); err != nil { diff --git a/backend/internal/hub/db/migrations/000016_create_notifications_tables.down.sql b/backend/internal/hub/db/migrations/000016_create_notifications_tables.down.sql new file mode 100644 index 0000000..c888f26 --- /dev/null +++ b/backend/internal/hub/db/migrations/000016_create_notifications_tables.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_application_notifications_notification_id; +DROP INDEX IF EXISTS idx_application_notifications_application_id; +DROP TABLE IF EXISTS application_notifications; +DROP TABLE IF EXISTS notifications; diff --git a/backend/internal/hub/db/migrations/000016_create_notifications_tables.up.sql b/backend/internal/hub/db/migrations/000016_create_notifications_tables.up.sql new file mode 100644 index 0000000..b71a8a4 --- /dev/null +++ b/backend/internal/hub/db/migrations/000016_create_notifications_tables.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS notifications ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + enable_by_default BOOLEAN NOT NULL DEFAULT 0, + status TEXT NOT NULL, + type TEXT NOT NULL, + config TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT (datetime('now')), + updated_at DATETIME NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS application_notifications ( + application_id TEXT NOT NULL, + notification_id TEXT NOT NULL, + PRIMARY KEY (application_id, notification_id), + FOREIGN KEY (application_id) REFERENCES applications(id) ON DELETE CASCADE, + FOREIGN KEY (notification_id) REFERENCES notifications(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_application_notifications_application_id + ON application_notifications (application_id); + +CREATE INDEX IF NOT EXISTS idx_application_notifications_notification_id + ON application_notifications (notification_id); diff --git a/backend/internal/hub/handlers.go b/backend/internal/hub/handlers.go index 46c1311..664189c 100644 --- a/backend/internal/hub/handlers.go +++ b/backend/internal/hub/handlers.go @@ -76,6 +76,12 @@ func RegisterRoutes(router *gin.Engine, cfg Config) error { protected.PUT("/agents/:id", routes.UpdateAgentHandler) protected.DELETE("/agents/:id", routes.DeleteAgentHandler) + protected.POST("/notifications", routes.CreateNotificationHandler) + protected.GET("/notifications", routes.ListNotificationsHandler) + protected.PUT("/notifications/:id", routes.UpdateNotificationHandler) + protected.DELETE("/notifications/:id", routes.DeleteNotificationHandler) + protected.POST("/notifications/:id/test", routes.TestNotificationHandler) + protected.GET("/events", routes.SSEHandler) } diff --git a/backend/internal/hub/models/applications.go b/backend/internal/hub/models/applications.go index 59fb753..ff1d70b 100644 --- a/backend/internal/hub/models/applications.go +++ b/backend/internal/hub/models/applications.go @@ -39,6 +39,7 @@ type Application struct { Path string `gorm:"type:text;not null"` ComposeFile crypto.EncryptedString `gorm:"type:text;not null"` PreviousComposeFile crypto.EncryptedString `gorm:"type:text;not null"` + Notifications []Notification `gorm:"many2many:application_notifications;"` } func (Application) TableName() string { diff --git a/backend/internal/hub/models/notifications.go b/backend/internal/hub/models/notifications.go new file mode 100644 index 0000000..51e7001 --- /dev/null +++ b/backend/internal/hub/models/notifications.go @@ -0,0 +1,32 @@ +package models + +import "github.com/OrcaCD/orca-cd/internal/hub/crypto" + +type NotificationStatus string + +const ( + NotificationStatusUnknown NotificationStatus = "unknown" + NotificationStatusSuccess NotificationStatus = "success" + NotificationStatusError NotificationStatus = "error" +) + +type NotificationType string + +const ( + NotificationTypeDiscord NotificationType = "discord" +) + +type Notification struct { + Base + Name crypto.EncryptedString `gorm:"type:text;not null"` + Enabled bool `gorm:"not null"` + EnableByDefault bool `gorm:"not null"` + Status NotificationStatus `gorm:"type:text;not null"` + Type NotificationType `gorm:"type:text;not null"` + Config crypto.EncryptedString `gorm:"type:text;not null"` + Applications []Application `gorm:"many2many:application_notifications;"` +} + +func (Notification) TableName() string { + return "notifications" +} diff --git a/backend/internal/hub/notifications/notifications.go b/backend/internal/hub/notifications/notifications.go new file mode 100644 index 0000000..6efb957 --- /dev/null +++ b/backend/internal/hub/notifications/notifications.go @@ -0,0 +1,161 @@ +package notifications + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/OrcaCD/orca-cd/internal/hub/db" + "github.com/OrcaCD/orca-cd/internal/hub/models" + "github.com/OrcaCD/orca-cd/internal/hub/notifications/provider" + "github.com/nicholas-fedor/shoutrrr" + "github.com/rs/zerolog" + "gorm.io/gorm" +) + +const notificationQueryTimeout = 10 * time.Second + +const DefaultTestNotificationMessage = "This is a test notification from OrcaCD." + +var ( + ErrInvalidNotificationConfig = errors.New("invalid notification config") + ErrNotificationDispatch = errors.New("notification dispatch failed") +) + +func SendNotification(applicationId string, message string, log *zerolog.Logger) { + if strings.TrimSpace(message) == "" { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), notificationQueryTimeout) + defer cancel() + + configs, err := getNotificationConfig(ctx, applicationId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + log.Warn().Str("applicationId", applicationId).Msg("application not found while sending notifications") + return + } + log.Error().Err(err).Str("applicationId", applicationId).Msg("failed to load notification config") + return + } + + for i := range configs { + targets, parseErr := provider.BuildShouterrrUrls(configs[i].Type, configs[i].Config.String()) + if parseErr != nil { + log.Error(). + Err(parseErr). + Str("applicationId", applicationId). + Str("notificationId", configs[i].Id). + Msg("failed to parse notification config") + setNotificationStatus(configs[i].Id, models.NotificationStatusError, log) + continue + } + + sender, createErr := shoutrrr.CreateSender(targets...) + if createErr != nil { + log.Error(). + Err(createErr). + Str("applicationId", applicationId). + Str("notificationId", configs[i].Id). + Msg("failed to create notification sender") + setNotificationStatus(configs[i].Id, models.NotificationStatusError, log) + continue + } + + sendErrs := sender.Send(message, nil) + hasSendError := false + for _, sendErr := range sendErrs { + if sendErr == nil { + continue + } + hasSendError = true + log.Error(). + Err(sendErr). + Str("applicationId", applicationId). + Str("notificationId", configs[i].Id). + Msg("failed to send notification") + } + + if hasSendError { + setNotificationStatus(configs[i].Id, models.NotificationStatusError, log) + continue + } + + setNotificationStatus(configs[i].Id, models.NotificationStatusSuccess, log) + } +} + +func getNotificationConfig(ctx context.Context, applicationId string) ([]models.Notification, error) { + _, err := gorm.G[models.Application](db.DB). + Select("id"). + Where("id = ?", applicationId). + First(ctx) + if err != nil { + return nil, err + } + + var notifications []models.Notification + err = db.DB.WithContext(ctx). + Table("notifications"). + Select("notifications.*"). + Joins("LEFT JOIN application_notifications ON application_notifications.notification_id = notifications.id"). + Where("notifications.enabled = ?", true). + Where("(notifications.enable_by_default = ? OR application_notifications.application_id = ?)", true, applicationId). + Group("notifications.id"). + Find(¬ifications).Error + if err != nil { + return nil, err + } + + return notifications, nil +} + +func SendTestNotification(notificationType models.NotificationType, rawConfig, message string) error { + targets, err := provider.BuildShouterrrUrls(notificationType, rawConfig) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidNotificationConfig, err) + } + + sender, err := shoutrrr.CreateSender(targets...) + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidNotificationConfig, err) + } + + msg := strings.TrimSpace(message) + if msg == "" { + msg = DefaultTestNotificationMessage + } + + sendErrs := sender.Send(msg, nil) + errList := make([]error, 0, len(sendErrs)) + for i := range sendErrs { + if sendErrs[i] != nil { + errList = append(errList, sendErrs[i]) + } + } + + if len(errList) > 0 { + return fmt.Errorf("%w: %w", ErrNotificationDispatch, errors.Join(errList...)) + } + + return nil +} + +func setNotificationStatus(notificationId string, status models.NotificationStatus, log *zerolog.Logger) { + ctx, cancel := context.WithTimeout(context.Background(), notificationQueryTimeout) + defer cancel() + + rowsAffected, err := gorm.G[models.Notification](db.DB). + Where("id = ?", notificationId). + Update(ctx, "status", status) + if err != nil { + log.Error().Err(err).Str("notificationId", notificationId).Str("status", string(status)).Msg("failed to update notification status") + return + } + if rowsAffected == 0 { + log.Warn().Str("notificationId", notificationId).Msg("notification not found while updating status") + } +} diff --git a/backend/internal/hub/notifications/notifications_send_test.go b/backend/internal/hub/notifications/notifications_send_test.go new file mode 100644 index 0000000..283a214 --- /dev/null +++ b/backend/internal/hub/notifications/notifications_send_test.go @@ -0,0 +1,173 @@ +package notifications + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/OrcaCD/orca-cd/internal/hub/crypto" + "github.com/OrcaCD/orca-cd/internal/hub/db" + "github.com/OrcaCD/orca-cd/internal/hub/models" + "github.com/rs/zerolog" + "gorm.io/gorm" +) + +func newNotificationLogger() *zerolog.Logger { + var sink bytes.Buffer + logger := zerolog.New(&sink) + return &logger +} + +func setNotificationConfig(t *testing.T, notificationId, rawConfig string) { + t.Helper() + + rowsAffected, err := gorm.G[models.Notification](db.DB). + Where("id = ?", notificationId). + Update(t.Context(), "config", crypto.EncryptedString(rawConfig)) + if err != nil { + t.Fatalf("failed to update notification config: %v", err) + } + if rowsAffected != 1 { + t.Fatalf("expected one row to be updated, got %d", rowsAffected) + } +} + +func assertNotificationStatus(t *testing.T, notificationId string, want models.NotificationStatus) { + t.Helper() + + notification, err := gorm.G[models.Notification](db.DB).Where("id = ?", notificationId).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification %s: %v", notificationId, err) + } + if notification.Status != want { + t.Fatalf("expected notification status %q, got %q", want, notification.Status) + } +} + +func TestGetNotificationConfigApplicationNotFound(t *testing.T) { + setupNotificationsTestDB(t) + + _, err := getNotificationConfig(context.Background(), "missing-application") + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected gorm.ErrRecordNotFound, got %v", err) + } +} + +func TestSendNotificationIgnoresEmptyMessage(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "empty-message", true, false, models.NotificationStatusUnknown, app.Id) + + SendNotification(app.Id, " ", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusUnknown) +} + +func TestSendNotificationApplicationNotFound(t *testing.T) { + setupNotificationsTestDB(t) + + notification := seedNotificationRecord(t, "default", true, true, models.NotificationStatusUnknown) + + SendNotification("missing-application", "ping", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusUnknown) +} + +func TestSendNotificationInvalidConfigMarksStatusError(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "invalid-config", true, false, models.NotificationStatusUnknown, app.Id) + setNotificationConfig(t, notification.Id, `{"webhookId":"123456789"}`) + + SendNotification(app.Id, "deploy done", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusError) +} + +func TestSendNotificationCreateSenderErrorMarksStatusError(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "create-sender-error", true, false, models.NotificationStatusUnknown, app.Id) + setNotificationConfig(t, notification.Id, "not-a-url") + + SendNotification(app.Id, "deploy done", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusError) +} + +func TestSendNotificationSuccessMarksStatusSuccess(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "send-success", true, false, models.NotificationStatusUnknown, app.Id) + setNotificationConfig(t, notification.Id, "logger://") + + SendNotification(app.Id, "deploy done", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusSuccess) +} + +func TestSendNotificationDispatchErrorMarksStatusError(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "send-error", true, false, models.NotificationStatusUnknown, app.Id) + setNotificationConfig(t, notification.Id, "generic+http://127.0.0.1:1") + + SendNotification(app.Id, "deploy done", newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusError) +} + +func TestSendTestNotificationInvalidConfig(t *testing.T) { + err := SendTestNotification(models.NotificationTypeDiscord, `{"webhookId":"123456789"}`, "ping") + if !errors.Is(err, ErrInvalidNotificationConfig) { + t.Fatalf("expected ErrInvalidNotificationConfig, got %v", err) + } +} + +func TestSendTestNotificationCreateSenderError(t *testing.T) { + err := SendTestNotification(models.NotificationTypeDiscord, "not-a-url", "ping") + if !errors.Is(err, ErrInvalidNotificationConfig) { + t.Fatalf("expected ErrInvalidNotificationConfig, got %v", err) + } +} + +func TestSendTestNotificationSuccessWithDefaultMessage(t *testing.T) { + err := SendTestNotification(models.NotificationTypeDiscord, "logger://", " ") + if err != nil { + t.Fatalf("expected successful test notification, got %v", err) + } +} + +func TestSendTestNotificationDispatchError(t *testing.T) { + err := SendTestNotification(models.NotificationTypeDiscord, "generic+http://127.0.0.1:1", "ping") + if !errors.Is(err, ErrNotificationDispatch) { + t.Fatalf("expected ErrNotificationDispatch, got %v", err) + } +} + +func TestSetNotificationStatus(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + notification := seedNotificationRecord(t, "set-status", true, false, models.NotificationStatusUnknown, app.Id) + + setNotificationStatus(notification.Id, models.NotificationStatusSuccess, newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusSuccess) +} + +func TestSetNotificationStatusNotificationNotFound(t *testing.T) { + setupNotificationsTestDB(t) + + notification := seedNotificationRecord(t, "missing-status", true, true, models.NotificationStatusUnknown) + + setNotificationStatus("missing-notification", models.NotificationStatusError, newNotificationLogger()) + + assertNotificationStatus(t, notification.Id, models.NotificationStatusUnknown) +} diff --git a/backend/internal/hub/notifications/notifications_test.go b/backend/internal/hub/notifications/notifications_test.go new file mode 100644 index 0000000..9cec0c1 --- /dev/null +++ b/backend/internal/hub/notifications/notifications_test.go @@ -0,0 +1,245 @@ +package notifications + +import ( + "context" + "log" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/OrcaCD/orca-cd/internal/hub/crypto" + "github.com/OrcaCD/orca-cd/internal/hub/db" + "github.com/OrcaCD/orca-cd/internal/hub/models" + "github.com/OrcaCD/orca-cd/internal/hub/notifications/provider" + "github.com/google/uuid" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" +) + +func setupNotificationsTestDB(t *testing.T) { + t.Helper() + + dbPath := filepath.Join(t.TempDir(), "test.db") + testDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: gormlogger.New( + log.New(os.Stderr, "\n", log.LstdFlags), + gormlogger.Config{LogLevel: gormlogger.Warn}, + ), + }) + if err != nil { + t.Fatalf("failed to open test db: %v", err) + } + + sqlDB, err := testDB.DB() + if err != nil { + t.Fatalf("failed to get sql db: %v", err) + } + t.Cleanup(func() { + _ = sqlDB.Close() + db.DB = nil + }) + + db.DB = testDB + + if err := crypto.Init("test-secret-that-is-long-enough-32chars"); err != nil { + t.Fatalf("failed to init crypto: %v", err) + } + + if err := db.DB.AutoMigrate(&models.Repository{}, &models.Agent{}, &models.Application{}, &models.Notification{}); err != nil { + t.Fatalf("failed to migrate models: %v", err) + } +} + +func seedNotificationTestApp(t *testing.T, healthStatus models.HealthStatus) models.Application { + t.Helper() + + repo := models.Repository{ + Name: "Repo", + Url: "https://github.com/orcacd/notifications-test-" + uuid.NewString(), + Provider: models.GitHub, + AuthMethod: models.AuthMethodNone, + SyncType: models.SyncTypeManual, + SyncStatus: models.SyncStatusUnknown, + CreatedBy: "user-1", + } + if err := db.DB.WithContext(t.Context()).Create(&repo).Error; err != nil { + t.Fatalf("failed to create repository: %v", err) + } + + agent := models.Agent{ + Name: crypto.EncryptedString("Agent"), + KeyId: crypto.EncryptedString("agent-key"), + Status: models.AgentStatusOffline, + } + if err := db.DB.WithContext(t.Context()).Create(&agent).Error; err != nil { + t.Fatalf("failed to create agent: %v", err) + } + + app := models.Application{ + Name: crypto.EncryptedString("App"), + RepositoryId: repo.Id, + AgentId: agent.Id, + SyncStatus: models.UnknownSync, + HealthStatus: healthStatus, + Branch: "main", + Commit: "abc123", + CommitMessage: "seed", + Path: "compose.yaml", + ComposeFile: crypto.EncryptedString("services: {}"), + PreviousComposeFile: crypto.EncryptedString(""), + } + if err := db.DB.WithContext(t.Context()).Select("*").Create(&app).Error; err != nil { + t.Fatalf("failed to create app: %v", err) + } + + return app +} + +func seedNotificationRecord(t *testing.T, name string, enabled, enableByDefault bool, status models.NotificationStatus, appIds ...string) models.Notification { + t.Helper() + + notification := models.Notification{ + Name: crypto.EncryptedString(name), + Enabled: enabled, + EnableByDefault: enableByDefault, + Status: status, + Type: models.NotificationTypeDiscord, + Config: crypto.EncryptedString("discord://token@channel"), + } + if err := db.DB.WithContext(t.Context()).Select("*").Create(¬ification).Error; err != nil { + t.Fatalf("failed to create notification: %v", err) + } + + if len(appIds) > 0 { + applications, err := gorm.G[models.Application](db.DB).Where("id IN ?", appIds).Find(t.Context()) + if err != nil { + t.Fatalf("failed to load notification applications: %v", err) + } + if err := db.DB.Model(¬ification).Association("Applications").Replace(applications); err != nil { + t.Fatalf("failed to associate applications: %v", err) + } + } + + return notification +} + +func TestGetNotificationConfig_FiltersByStatusAndAssociation(t *testing.T) { + setupNotificationsTestDB(t) + + app := seedNotificationTestApp(t, models.Healthy) + otherApp := seedNotificationTestApp(t, models.Unhealthy) + + associated := seedNotificationRecord(t, "associated", true, false, models.NotificationStatusUnknown, app.Id) + defaultUnknown := seedNotificationRecord(t, "default-unknown", true, true, models.NotificationStatusUnknown) + withErrorStatus := seedNotificationRecord(t, "with-error-status", true, true, models.NotificationStatusError) + seedNotificationRecord(t, "disabled", false, true, models.NotificationStatusSuccess) + seedNotificationRecord(t, "other-app", true, false, models.NotificationStatusSuccess, otherApp.Id) + + configs, err := getNotificationConfig(context.Background(), app.Id) + if err != nil { + t.Fatalf("getNotificationConfig() error: %v", err) + } + + ids := make([]string, 0, len(configs)) + for i := range configs { + ids = append(ids, configs[i].Id) + } + + if !slices.Contains(ids, associated.Id) { + t.Fatalf("expected associated notification in result, ids=%v", ids) + } + if !slices.Contains(ids, defaultUnknown.Id) { + t.Fatalf("expected default unknown notification in result, ids=%v", ids) + } + if !slices.Contains(ids, withErrorStatus.Id) { + t.Fatalf("expected default error-status notification in result, ids=%v", ids) + } + if len(ids) != 3 { + t.Fatalf("expected exactly 3 matching notifications, got %d (%v)", len(ids), ids) + } +} + +func TestBuildShouterrrUrls_DirectTargets(t *testing.T) { + tests := []struct { + name string + raw string + expects []string + }{ + { + name: "single raw URL", + raw: "discord://token@channel", + expects: []string{"discord://token@channel"}, + }, + { + name: "comma separated URLs", + raw: "discord://a@1, discord://b@2", + expects: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "JSON object config", + raw: `{"url":"discord://a@1","urls":["discord://b@2"]}`, + expects: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "JSON array config", + raw: `["discord://a@1","discord://b@2"]`, + expects: []string{"discord://a@1", "discord://b@2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + urls, err := provider.BuildShouterrrUrls(models.NotificationTypeDiscord, tt.raw) + if err != nil { + t.Fatalf("BuildShouterrrUrls() error: %v", err) + } + if !slices.Equal(urls, tt.expects) { + t.Fatalf("expected %v, got %v", tt.expects, urls) + } + }) + } +} + +func TestBuildShouterrrUrls_DiscordObjectConfig(t *testing.T) { + urls, err := provider.BuildShouterrrUrls(models.NotificationTypeDiscord, `{"token":"token-abc","webhookId":"123456789","threadId":"987654321"}`) + if err != nil { + t.Fatalf("BuildShouterrrUrls() error: %v", err) + } + + if len(urls) != 1 { + t.Fatalf("expected 1 URL, got %d", len(urls)) + } + if !strings.HasPrefix(urls[0], "discord://token-abc@123456789") { + t.Fatalf("expected discord URL, got %s", urls[0]) + } + if !strings.Contains(urls[0], "thread_id=987654321") { + t.Fatalf("expected thread_id in URL, got %s", urls[0]) + } +} + +func TestBuildShouterrrUrls_DiscordObjectConfigMissingFields(t *testing.T) { + _, err := provider.BuildShouterrrUrls(models.NotificationTypeDiscord, `{"webhookId":"123456789"}`) + if err == nil { + t.Fatal("expected error for missing discord token") + } +} + +func TestGetProvider_Registered(t *testing.T) { + provider, err := provider.Get(models.NotificationTypeDiscord) + if err != nil { + t.Fatalf("Get() error: %v", err) + } + if provider == nil { + t.Fatal("expected provider to be non-nil") + } +} + +func TestGetProvider_Unregistered(t *testing.T) { + _, err := provider.Get(models.NotificationType("custom")) + if err == nil { + t.Fatal("expected error for unregistered provider") + } +} diff --git a/backend/internal/hub/notifications/provider/common.go b/backend/internal/hub/notifications/provider/common.go new file mode 100644 index 0000000..35f5a9a --- /dev/null +++ b/backend/internal/hub/notifications/provider/common.go @@ -0,0 +1,85 @@ +package provider + +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + +type directTargetsConfig struct { + URL string `json:"url"` + URLs []string `json:"urls"` +} + +func normalizeRawConfig(rawConfig string) (string, error) { + trimmed := strings.TrimSpace(rawConfig) + if trimmed == "" { + return "", errors.New("notification config is empty") + } + + return trimmed, nil +} + +func decodeConfigJSON[T any](trimmed string, invalidMessage string) (T, error) { + var value T + if err := json.Unmarshal([]byte(trimmed), &value); err != nil { + return value, fmt.Errorf("%s: %w", invalidMessage, err) + } + + return value, nil +} + +func parseDirectTargets(rawConfig string) ([]string, error) { + trimmed, err := normalizeRawConfig(rawConfig) + if err != nil { + return nil, err + } + + if strings.HasPrefix(trimmed, "{") { + cfg, err := decodeConfigJSON[directTargetsConfig](trimmed, "invalid JSON notification config") + if err != nil { + return nil, err + } + + return normalizeTargets(append([]string{cfg.URL}, cfg.URLs...)) + } + + if strings.HasPrefix(trimmed, "[") { + urls, err := decodeConfigJSON[[]string](trimmed, "invalid JSON notification URL list") + if err != nil { + return nil, err + } + + return normalizeTargets(urls) + } + + parts := strings.FieldsFunc(trimmed, func(r rune) bool { + return r == ',' || r == '\n' || r == '\r' + }) + + return normalizeTargets(parts) +} + +func normalizeTargets(rawTargets []string) ([]string, error) { + targets := make([]string, 0, len(rawTargets)) + seen := make(map[string]struct{}, len(rawTargets)) + + for i := range rawTargets { + target := strings.TrimSpace(rawTargets[i]) + if target == "" { + continue + } + if _, exists := seen[target]; exists { + continue + } + seen[target] = struct{}{} + targets = append(targets, target) + } + + if len(targets) == 0 { + return nil, errors.New("notification config does not contain any targets") + } + + return targets, nil +} diff --git a/backend/internal/hub/notifications/provider/common_test.go b/backend/internal/hub/notifications/provider/common_test.go new file mode 100644 index 0000000..23abba4 --- /dev/null +++ b/backend/internal/hub/notifications/provider/common_test.go @@ -0,0 +1,116 @@ +package provider + +import ( + "slices" + "strings" + "testing" +) + +func TestParseDirectTargets(t *testing.T) { + tests := []struct { + name string + raw string + wants []string + wantErr string + }{ + { + name: "empty config", + raw: " \n\t ", + wantErr: "notification config is empty", + }, + { + name: "invalid json object", + raw: "{", + wantErr: "invalid JSON notification config", + }, + { + name: "invalid json list", + raw: "[", + wantErr: "invalid JSON notification URL list", + }, + { + name: "json object", + raw: `{"url":" discord://a@1 ","urls":["discord://b@2","discord://a@1","", " "]}`, + wants: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "json list", + raw: `["discord://a@1", " discord://a@1 ", "discord://b@2"]`, + wants: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "comma and newline delimited", + raw: "discord://a@1,\n discord://b@2\r,discord://a@1", + wants: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "no usable targets", + raw: "\n\r , ,, ", + wantErr: "notification config does not contain any targets", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseDirectTargets(tt.raw) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("parseDirectTargets() error = %v", err) + } + if !slices.Equal(got, tt.wants) { + t.Fatalf("expected %v, got %v", tt.wants, got) + } + }) + } +} + +func TestNormalizeTargets(t *testing.T) { + tests := []struct { + name string + raw []string + wants []string + wantErr string + }{ + { + name: "trim and deduplicate", + raw: []string{" discord://a@1 ", "", "discord://a@1", "discord://b@2", " discord://b@2 "}, + wants: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "all empty", + raw: []string{" ", "", "\t"}, + wantErr: "notification config does not contain any targets", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeTargets(tt.raw) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("normalizeTargets() error = %v", err) + } + if !slices.Equal(got, tt.wants) { + t.Fatalf("expected %v, got %v", tt.wants, got) + } + }) + } +} diff --git a/backend/internal/hub/notifications/provider/discord.go b/backend/internal/hub/notifications/provider/discord.go new file mode 100644 index 0000000..2e89422 --- /dev/null +++ b/backend/internal/hub/notifications/provider/discord.go @@ -0,0 +1,69 @@ +package provider + +import ( + "errors" + "net/url" + "strings" +) + +type DiscordProvider struct{} + +type discordConfig struct { + Token string `json:"token"` + WebhookID string `json:"webhookId"` + ThreadID string `json:"threadId"` + Username string `json:"username"` + AvatarURL string `json:"avatarUrl"` + Title string `json:"title"` + URL string `json:"url"` + URLs []string `json:"urls"` +} + +func (DiscordProvider) BuildShouterrrUrls(rawConfig string) ([]string, error) { + trimmed, err := normalizeRawConfig(rawConfig) + if err != nil { + return nil, err + } + + if strings.HasPrefix(trimmed, "{") { + cfg, err := decodeConfigJSON[discordConfig](trimmed, "invalid JSON discord config") + if err != nil { + return nil, err + } + + if cfg.URL != "" || len(cfg.URLs) > 0 { + return normalizeTargets(append([]string{cfg.URL}, cfg.URLs...)) + } + + token := strings.TrimSpace(cfg.Token) + webhookID := strings.TrimSpace(cfg.WebhookID) + if token == "" || webhookID == "" { + return nil, errors.New("discord config requires token and webhookId") + } + + query := url.Values{} + if v := strings.TrimSpace(cfg.ThreadID); v != "" { + query.Set("thread_id", v) + } + if v := strings.TrimSpace(cfg.Username); v != "" { + query.Set("username", v) + } + if v := strings.TrimSpace(cfg.AvatarURL); v != "" { + query.Set("avatarurl", v) + } + if v := strings.TrimSpace(cfg.Title); v != "" { + query.Set("title", v) + } + + discordURL := url.URL{ + Scheme: "discord", + User: url.User(token), + Host: webhookID, + RawQuery: query.Encode(), + } + + return []string{discordURL.String()}, nil + } + + return parseDirectTargets(trimmed) +} diff --git a/backend/internal/hub/notifications/provider/discord_test.go b/backend/internal/hub/notifications/provider/discord_test.go new file mode 100644 index 0000000..240429c --- /dev/null +++ b/backend/internal/hub/notifications/provider/discord_test.go @@ -0,0 +1,108 @@ +package provider + +import ( + "net/url" + "slices" + "strings" + "testing" +) + +func TestDiscordProviderBuildShouterrrUrls(t *testing.T) { + tests := []struct { + name string + raw string + wants []string + wantErr string + }{ + { + name: "empty config", + raw: " \n ", + wantErr: "notification config is empty", + }, + { + name: "invalid json object", + raw: "{", + wantErr: "invalid JSON discord config", + }, + { + name: "json object with direct urls", + raw: `{"url":"discord://a@1","urls":[" discord://b@2 ","discord://a@1"]}`, + wants: []string{"discord://a@1", "discord://b@2"}, + }, + { + name: "json object missing token", + raw: `{"webhookId":"123"}`, + wantErr: "discord config requires token and webhookId", + }, + { + name: "direct target fallback", + raw: "discord://a@1,discord://b@2", + wants: []string{"discord://a@1", "discord://b@2"}, + }, + } + + provider := DiscordProvider{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := provider.BuildShouterrrUrls(tt.raw) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("BuildShouterrrUrls() error = %v", err) + } + if !slices.Equal(got, tt.wants) { + t.Fatalf("expected %v, got %v", tt.wants, got) + } + }) + } +} + +func TestDiscordProviderBuildsStructuredURL(t *testing.T) { + provider := DiscordProvider{} + + urls, err := provider.BuildShouterrrUrls(`{"token":"token-abc","webhookId":"123456789","threadId":"987654321","username":"Orca Bot","avatarUrl":"https://example.com/avatar.png","title":"Deploy done"}`) + if err != nil { + t.Fatalf("BuildShouterrrUrls() error = %v", err) + } + if len(urls) != 1 { + t.Fatalf("expected 1 URL, got %d", len(urls)) + } + + parsed, err := url.Parse(urls[0]) + if err != nil { + t.Fatalf("failed to parse URL %q: %v", urls[0], err) + } + + if parsed.Scheme != "discord" { + t.Fatalf("expected scheme discord, got %q", parsed.Scheme) + } + if parsed.User == nil || parsed.User.Username() != "token-abc" { + t.Fatalf("expected token in URL userinfo, got %v", parsed.User) + } + if parsed.Host != "123456789" { + t.Fatalf("expected webhook id host %q, got %q", "123456789", parsed.Host) + } + + query := parsed.Query() + if query.Get("thread_id") != "987654321" { + t.Fatalf("expected thread_id query parameter, got %q", query.Get("thread_id")) + } + if query.Get("username") != "Orca Bot" { + t.Fatalf("expected username query parameter, got %q", query.Get("username")) + } + if query.Get("avatarurl") != "https://example.com/avatar.png" { + t.Fatalf("expected avatarurl query parameter, got %q", query.Get("avatarurl")) + } + if query.Get("title") != "Deploy done" { + t.Fatalf("expected title query parameter, got %q", query.Get("title")) + } +} diff --git a/backend/internal/hub/notifications/provider/provider.go b/backend/internal/hub/notifications/provider/provider.go new file mode 100644 index 0000000..cc70a10 --- /dev/null +++ b/backend/internal/hub/notifications/provider/provider.go @@ -0,0 +1,38 @@ +package provider + +import ( + "fmt" + + "github.com/OrcaCD/orca-cd/internal/hub/models" +) + +type Provider interface { + BuildShouterrrUrls(rawConfig string) ([]string, error) +} + +var registry = map[models.NotificationType]Provider{} + +func init() { + Register(models.NotificationTypeDiscord, DiscordProvider{}) +} + +func Register(notificationType models.NotificationType, provider Provider) { + registry[notificationType] = provider +} + +func Get(notificationType models.NotificationType) (Provider, error) { + provider, ok := registry[notificationType] + if !ok { + return nil, fmt.Errorf("no provider registered for notification type %q", notificationType) + } + return provider, nil +} + +func BuildShouterrrUrls(notificationType models.NotificationType, rawConfig string) ([]string, error) { + provider, err := Get(notificationType) + if err != nil { + return nil, err + } + + return provider.BuildShouterrrUrls(rawConfig) +} diff --git a/backend/internal/hub/notifications/provider/provider_test.go b/backend/internal/hub/notifications/provider/provider_test.go new file mode 100644 index 0000000..4eb1283 --- /dev/null +++ b/backend/internal/hub/notifications/provider/provider_test.go @@ -0,0 +1,72 @@ +package provider + +import ( + "maps" + "slices" + "strings" + "testing" + + "github.com/OrcaCD/orca-cd/internal/hub/models" +) + +type staticProvider struct { + urls []string +} + +func (p staticProvider) BuildShouterrrUrls(string) ([]string, error) { + return p.urls, nil +} + +func cloneProviderRegistry(src map[models.NotificationType]Provider) map[models.NotificationType]Provider { + cloned := make(map[models.NotificationType]Provider, len(src)) + maps.Copy(cloned, src) + + return cloned +} + +func TestGetDefaultProvider(t *testing.T) { + provider, err := Get(models.NotificationTypeDiscord) + if err != nil { + t.Fatalf("Get() error = %v", err) + } + if provider == nil { + t.Fatal("expected default discord provider to be registered") + } +} + +func TestRegisterAndGet(t *testing.T) { + originalRegistry := cloneProviderRegistry(registry) + t.Cleanup(func() { + registry = cloneProviderRegistry(originalRegistry) + }) + + registry = map[models.NotificationType]Provider{} + + notificationType := models.NotificationType("provider-register-test") + expected := []string{"test://a", "test://b"} + + Register(notificationType, staticProvider{urls: expected}) + + gotProvider, err := Get(notificationType) + if err != nil { + t.Fatalf("Get() error = %v", err) + } + + gotURLs, err := gotProvider.BuildShouterrrUrls("ignored") + if err != nil { + t.Fatalf("BuildShouterrrUrls() error = %v", err) + } + if !slices.Equal(gotURLs, expected) { + t.Fatalf("expected %v, got %v", expected, gotURLs) + } +} + +func TestGetUnregisteredProvider(t *testing.T) { + _, err := Get(models.NotificationType("definitely-missing-provider")) + if err == nil { + t.Fatal("expected error for missing provider") + } + if !strings.Contains(err.Error(), "no provider registered") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/backend/internal/hub/routes/applications.go b/backend/internal/hub/routes/applications.go index 68529e6..05cedd4 100644 --- a/backend/internal/hub/routes/applications.go +++ b/backend/internal/hub/routes/applications.go @@ -66,6 +66,12 @@ type applicationResponse struct { PreviousComposeFile string `json:"previousComposeFile,omitempty"` } +// Represents the many-to-many relationship between applications and notifications +type ApplicationNotification struct { + ApplicationId string `gorm:"primaryKey"` + NotificationId string `gorm:"primaryKey"` +} + func ListApplicationsHandler(c *gin.Context) { applications, err := gorm.G[models.Application](db.DB). Preload("Repository", nil). @@ -174,6 +180,23 @@ func CreateApplicationHandler(c *gin.Context) { c.JSON(http.StatusCreated, toApplicationResponse(&createdApplication)) sse.PublishUpdate(ApplicationsPath) + + var notifications []models.Notification + if err := db.DB.Where("enable_by_default = ?", true).Find(¬ifications).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + for _, notification := range notifications { + association := ApplicationNotification{ + ApplicationId: application.Id, + NotificationId: notification.Id, + } + if err := db.DB.Create(&association).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to associate notifications"}) + return + } + } } func UpdateApplicationHandler(c *gin.Context) { diff --git a/backend/internal/hub/routes/notifications.go b/backend/internal/hub/routes/notifications.go new file mode 100644 index 0000000..c78c0fa --- /dev/null +++ b/backend/internal/hub/routes/notifications.go @@ -0,0 +1,518 @@ +package routes + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/OrcaCD/orca-cd/internal/hub/crypto" + "github.com/OrcaCD/orca-cd/internal/hub/db" + "github.com/OrcaCD/orca-cd/internal/hub/models" + hubnotifications "github.com/OrcaCD/orca-cd/internal/hub/notifications" + "github.com/OrcaCD/orca-cd/internal/hub/notifications/provider" + "github.com/OrcaCD/orca-cd/internal/hub/sse" + "github.com/gin-gonic/gin" + "github.com/nicholas-fedor/shoutrrr" + "gorm.io/gorm" +) + +const NotificationsPath = "/api/v1/notifications" + +type createNotificationRequest struct { + Name string `json:"name" binding:"required,min=1,max=128"` + Enabled *bool `json:"enabled"` + EnableByDefault *bool `json:"enableByDefault"` + Type models.NotificationType `json:"type" binding:"required"` + Config json.RawMessage `json:"config" binding:"required"` + ApplicationIds []string `json:"applicationIds"` +} + +type updateNotificationRequest struct { + Name string `json:"name" binding:"required,min=1,max=128"` + Enabled *bool `json:"enabled"` + EnableByDefault *bool `json:"enableByDefault"` + Type models.NotificationType `json:"type" binding:"required"` + Config json.RawMessage `json:"config" binding:"required"` + ApplicationIds []string `json:"applicationIds"` +} + +type testNotificationRequest struct { + Message string `json:"message"` +} + +var sendTestNotification = hubnotifications.SendTestNotification + +type notificationResponse struct { + Id string `json:"id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + EnableByDefault bool `json:"enableByDefault"` + Status string `json:"status"` + Type string `json:"type"` + Config *string `json:"config,omitempty"` + ApplicationIds []string `json:"applicationIds"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func ListNotificationsHandler(c *gin.Context) { + includeConfig, err := parseIncludeConfigQuery(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + items, err := gorm.G[models.Notification](db.DB). + Preload("Applications", nil). + Order("created_at ASC"). + Find(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + response := make([]notificationResponse, 0, len(items)) + for i := range items { + response = append(response, toNotificationResponse(&items[i], includeConfig)) + } + + c.JSON(http.StatusOK, response) +} + +func CreateNotificationHandler(c *gin.Context) { + var req createNotificationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: name, type and config are required"}) + return + } + + normalizedName, err := normalizeNotificationName(req.Name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if !isValidNotificationType(req.Type) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid type"}) + return + } + + normalizedConfig, err := normalizeNotificationConfig(req.Config) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateNotificationConfig(req.Type, normalizedConfig); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid config: " + err.Error()}) + return + } + + ctx := c.Request.Context() + applications, missingApplicationId, err := loadNotificationApplications(ctx, req.ApplicationIds) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + if missingApplicationId != "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "application not found: " + missingApplicationId}) + return + } + + enabled := true + if req.Enabled != nil { + enabled = *req.Enabled + } + enableByDefault := false + if req.EnableByDefault != nil { + enableByDefault = *req.EnableByDefault + } + + notification := models.Notification{ + Name: crypto.EncryptedString(normalizedName), + Enabled: enabled, + EnableByDefault: enableByDefault, + Status: models.NotificationStatusUnknown, + Type: req.Type, + Config: crypto.EncryptedString(normalizedConfig), + } + + err = db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := gorm.G[models.Notification](tx).Select("*").Create(ctx, ¬ification); err != nil { + return err + } + + if err := tx.Model(¬ification).Association("Applications").Replace(applications); err != nil { + return err + } + + return nil + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + createdNotification, err := getNotificationById(ctx, notification.Id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + c.JSON(http.StatusCreated, toNotificationResponse(&createdNotification, true)) + sse.PublishUpdate(NotificationsPath) +} + +func UpdateNotificationHandler(c *gin.Context) { + id := c.Param("id") + + var req updateNotificationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: name, type and config are required"}) + return + } + + normalizedName, err := normalizeNotificationName(req.Name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if !isValidNotificationType(req.Type) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid type"}) + return + } + + normalizedConfig, err := normalizeNotificationConfig(req.Config) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := validateNotificationConfig(req.Type, normalizedConfig); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid config: " + err.Error()}) + return + } + + ctx := c.Request.Context() + + existingNotification, err := getNotificationById(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "notification not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + applications, missingApplicationId, err := loadNotificationApplications(ctx, req.ApplicationIds) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + if missingApplicationId != "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "application not found: " + missingApplicationId}) + return + } + + enabled := existingNotification.Enabled + if req.Enabled != nil { + enabled = *req.Enabled + } + enableByDefault := existingNotification.EnableByDefault + if req.EnableByDefault != nil { + enableByDefault = *req.EnableByDefault + } + + updates := models.Notification{ + Name: crypto.EncryptedString(normalizedName), + Enabled: enabled, + EnableByDefault: enableByDefault, + Status: models.NotificationStatusUnknown, + Type: req.Type, + Config: crypto.EncryptedString(normalizedConfig), + } + + err = db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + rowsAffected, err := gorm.G[models.Notification](tx). + Where("id = ?", id). + Select("name", "enabled", "enable_by_default", "status", "type", "config"). + Updates(ctx, updates) + if err != nil { + return err + } + if rowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + if err := tx.Model(&existingNotification).Association("Applications").Replace(applications); err != nil { + return err + } + + return nil + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "notification not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + updatedNotification, err := getNotificationById(ctx, id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + c.JSON(http.StatusOK, toNotificationResponse(&updatedNotification, true)) + sse.PublishUpdate(NotificationsPath) +} + +func DeleteNotificationHandler(c *gin.Context) { + id := c.Param("id") + + rowsAffected, err := gorm.G[models.Notification](db.DB).Where("id = ?", id).Delete(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + if rowsAffected == 0 { + c.JSON(http.StatusNotFound, gin.H{"error": "notification not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "notification deleted"}) + sse.PublishUpdate(NotificationsPath) +} + +func TestNotificationHandler(c *gin.Context) { + id := c.Param("id") + ctx := c.Request.Context() + + notification, err := gorm.G[models.Notification](db.DB).Where("id = ?", id).First(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "notification not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + var req testNotificationRequest + if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: message must be a string"}) + return + } + + sendErr := sendTestNotification(notification.Type, notification.Config.String(), req.Message) + status := models.NotificationStatusSuccess + if sendErr != nil { + status = models.NotificationStatusError + } + + if err := updateNotificationStatus(ctx, notification.Id, status); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "notification not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + return + } + + sse.PublishUpdate(NotificationsPath) + + if sendErr != nil { + switch { + case errors.Is(sendErr, hubnotifications.ErrInvalidNotificationConfig): + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid config: " + sendErr.Error()}) + case errors.Is(sendErr, hubnotifications.ErrNotificationDispatch): + c.JSON(http.StatusBadGateway, gin.H{"error": "failed to send test notification"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "test notification sent"}) +} + +func updateNotificationStatus(ctx context.Context, id string, status models.NotificationStatus) error { + rowsAffected, err := gorm.G[models.Notification](db.DB). + Where("id = ?", id). + Update(ctx, "status", status) + if err != nil { + return err + } + if rowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return nil +} + +func getNotificationById(ctx context.Context, id string) (models.Notification, error) { + return gorm.G[models.Notification](db.DB). + Preload("Applications", nil). + Where("id = ?", id). + First(ctx) +} + +func toNotificationResponse(notification *models.Notification, includeConfig bool) notificationResponse { + applicationIds := make([]string, 0, len(notification.Applications)) + for i := range notification.Applications { + applicationIds = append(applicationIds, notification.Applications[i].Id) + } + sort.Strings(applicationIds) + + response := notificationResponse{ + Id: notification.Id, + Name: notification.Name.String(), + Enabled: notification.Enabled, + EnableByDefault: notification.EnableByDefault, + Status: string(notification.Status), + Type: string(notification.Type), + ApplicationIds: applicationIds, + CreatedAt: notification.CreatedAt.Format(time.RFC3339), + UpdatedAt: notification.UpdatedAt.Format(time.RFC3339), + } + + if includeConfig { + config := notification.Config.String() + response.Config = &config + } + + return response +} + +func isValidNotificationType(notificationType models.NotificationType) bool { + _, err := provider.Get(notificationType) + return err == nil +} + +func parseIncludeConfigQuery(c *gin.Context) (bool, error) { + raw := strings.TrimSpace(c.Query("includeConfig")) + if raw == "" { + return false, nil + } + + includeConfig, err := strconv.ParseBool(raw) + if err != nil { + return false, errors.New("invalid includeConfig: must be a boolean") + } + + return includeConfig, nil +} + +func normalizeNotificationName(rawName string) (string, error) { + trimmedName := strings.TrimSpace(rawName) + if trimmedName == "" { + return "", errors.New("invalid name: must not be empty") + } + + if utf8.RuneCountInString(trimmedName) > 128 { + return "", errors.New("invalid name: must be at most 128 characters") + } + + return trimmedName, nil +} + +func validateNotificationConfig(notificationType models.NotificationType, rawConfig string) error { + targets, err := provider.BuildShouterrrUrls(notificationType, rawConfig) + if err != nil { + return err + } + + if _, err := shoutrrr.CreateSender(targets...); err != nil { + return err + } + + return nil +} + +func normalizeNotificationConfig(raw json.RawMessage) (string, error) { + trimmed := strings.TrimSpace(string(raw)) + if trimmed == "" { + return "", errors.New("invalid config: must be a non-empty string or JSON object") + } + + var value any + if err := json.Unmarshal(raw, &value); err != nil { + if strings.HasPrefix(trimmed, "\"") { + return "", errors.New("invalid config: expected a valid JSON string") + } + + return "", errors.New("invalid config: expected valid JSON") + } + + if value == nil { + return "", errors.New("invalid config: must be a non-empty string or JSON object") + } + + if stringValue, ok := value.(string); ok { + stringValue = strings.TrimSpace(stringValue) + if stringValue == "" { + return "", errors.New("invalid config: must not be empty") + } + + return stringValue, nil + } + + return trimmed, nil +} + +func normalizeNotificationApplicationIds(applicationIds []string) []string { + if len(applicationIds) == 0 { + return nil + } + + normalized := make([]string, 0, len(applicationIds)) + seen := make(map[string]struct{}, len(applicationIds)) + + for i := range applicationIds { + id := strings.TrimSpace(applicationIds[i]) + if id == "" { + continue + } + if _, exists := seen[id]; exists { + continue + } + seen[id] = struct{}{} + normalized = append(normalized, id) + } + + return normalized +} + +func loadNotificationApplications(ctx context.Context, applicationIds []string) ([]models.Application, string, error) { + normalizedIds := normalizeNotificationApplicationIds(applicationIds) + if len(normalizedIds) == 0 { + return []models.Application{}, "", nil + } + + applications, err := gorm.G[models.Application](db.DB).Where("id IN ?", normalizedIds).Find(ctx) + if err != nil { + return nil, "", err + } + if len(applications) != len(normalizedIds) { + foundById := make(map[string]struct{}, len(applications)) + for i := range applications { + foundById[applications[i].Id] = struct{}{} + } + for i := range normalizedIds { + if _, ok := foundById[normalizedIds[i]]; !ok { + return nil, normalizedIds[i], nil + } + } + } + + return applications, "", nil +} diff --git a/backend/internal/hub/routes/notifications_test.go b/backend/internal/hub/routes/notifications_test.go new file mode 100644 index 0000000..380dc5a --- /dev/null +++ b/backend/internal/hub/routes/notifications_test.go @@ -0,0 +1,1074 @@ +package routes + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "slices" + "strings" + "testing" + + "github.com/OrcaCD/orca-cd/internal/hub/crypto" + "github.com/OrcaCD/orca-cd/internal/hub/db" + "github.com/OrcaCD/orca-cd/internal/hub/models" + hubnotifications "github.com/OrcaCD/orca-cd/internal/hub/notifications" + "github.com/OrcaCD/orca-cd/internal/hub/notifications/provider" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func setupTestDBWithNotifications(t *testing.T) { + t.Helper() + setupTestDB(t) + if err := db.DB.AutoMigrate(&models.Repository{}, &models.Agent{}, &models.Application{}, &models.Notification{}); err != nil { + t.Fatalf("failed to migrate Notification/Repository/Agent/Application: %v", err) + } +} + +func seedNotificationRepository(t *testing.T) models.Repository { + t.Helper() + + repository := models.Repository{ + Name: "Repo", + Url: "https://github.com/orcacd/notifications", + Provider: models.GitHub, + AuthMethod: models.AuthMethodNone, + SyncType: models.SyncTypeManual, + SyncStatus: models.SyncStatusUnknown, + CreatedBy: "user-1", + } + + if err := db.DB.WithContext(t.Context()).Create(&repository).Error; err != nil { + t.Fatalf("failed to seed repository: %v", err) + } + + return repository +} + +func seedNotificationAgent(t *testing.T) models.Agent { + t.Helper() + + agent := models.Agent{ + Name: crypto.EncryptedString("Notifications Agent"), + KeyId: crypto.EncryptedString("notifications-key"), + Status: models.AgentStatusOffline, + } + + if err := db.DB.WithContext(t.Context()).Create(&agent).Error; err != nil { + t.Fatalf("failed to seed agent: %v", err) + } + + return agent +} + +func seedNotificationApplication(t *testing.T, repositoryId, agentId, name string) models.Application { + t.Helper() + + application := models.Application{ + Name: crypto.EncryptedString(name), + RepositoryId: repositoryId, + AgentId: agentId, + SyncStatus: models.UnknownSync, + HealthStatus: models.UnknownHealth, + Branch: "main", + Commit: "abc123", + CommitMessage: "seed commit", + Path: "compose.yaml", + ComposeFile: crypto.EncryptedString("services: {}"), + PreviousComposeFile: crypto.EncryptedString(""), + } + + if err := db.DB.WithContext(t.Context()).Select("*").Create(&application).Error; err != nil { + t.Fatalf("failed to seed application: %v", err) + } + + return application +} + +func createNotificationRecord(t *testing.T, applicationIds []string) models.Notification { + t.Helper() + + notification := models.Notification{ + Name: crypto.EncryptedString("Initial Notification"), + Enabled: true, + EnableByDefault: false, + Status: models.NotificationStatusUnknown, + Type: models.NotificationTypeDiscord, + Config: crypto.EncryptedString("discord://token@channel"), + } + + if err := db.DB.WithContext(t.Context()).Select("*").Create(¬ification).Error; err != nil { + t.Fatalf("failed to create notification: %v", err) + } + + if len(applicationIds) > 0 { + applications, err := gorm.G[models.Application](db.DB).Where("id IN ?", applicationIds).Find(t.Context()) + if err != nil { + t.Fatalf("failed to load applications for notification: %v", err) + } + if err := db.DB.Model(¬ification).Association("Applications").Replace(applications); err != nil { + t.Fatalf("failed to attach applications to notification: %v", err) + } + } + + return notification +} + +func TestListNotificationsHandler_Empty(t *testing.T) { + setupTestDBWithNotifications(t) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil) + + ListNotificationsHandler(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var body []notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(body) != 0 { + t.Fatalf("expected 0 notifications, got %d", len(body)) + } +} + +func TestListNotificationsHandler_ReturnsNotifications(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + appA := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + appB := seedNotificationApplication(t, repo.Id, agent.Id, "App B") + createNotificationRecord(t, []string{appA.Id, appB.Id}) + createNotificationRecord(t, []string{appB.Id}) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil) + + ListNotificationsHandler(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var body []notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(body) != 2 { + t.Fatalf("expected 2 notifications, got %d", len(body)) + } + + for i := range body { + if body[i].Config != nil { + t.Fatal("expected config to be omitted by default in list response") + } + } +} + +func TestListNotificationsHandler_IncludeConfig(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + app := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + createNotificationRecord(t, []string{app.Id}) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/notifications?includeConfig=true", nil) + + ListNotificationsHandler(c) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var body []notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if len(body) != 1 { + t.Fatalf("expected 1 notification, got %d", len(body)) + } + if body[0].Config == nil || *body[0].Config == "" { + t.Fatal("expected config to be included when includeConfig=true") + } +} + +func TestListNotificationsHandler_InvalidIncludeConfig(t *testing.T) { + setupTestDBWithNotifications(t) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/notifications?includeConfig=not-bool", nil) + + ListNotificationsHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestCreateNotificationHandler_Success(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + appA := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + appB := seedNotificationApplication(t, repo.Id, agent.Id, "App B") + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Deploy Alerts", + "enabled": true, + "enableByDefault": false, + "type": "discord", + "config": "discord://token@channel", + "applicationIds": []string{appA.Id, appB.Id}, + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + + var body notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + if body.Name != "Deploy Alerts" { + t.Fatalf("expected name %q, got %q", "Deploy Alerts", body.Name) + } + if body.Status != string(models.NotificationStatusUnknown) { + t.Fatalf("expected status %q, got %q", models.NotificationStatusUnknown, body.Status) + } + if body.Type != "discord" { + t.Fatalf("expected type %q, got %q", "discord", body.Type) + } + if body.Config == nil || *body.Config != "discord://token@channel" { + t.Fatalf("expected config to be returned in create response, got %#v", body.Config) + } + if len(body.ApplicationIds) != 2 { + t.Fatalf("expected 2 applicationIds, got %d", len(body.ApplicationIds)) + } + if !slices.Contains(body.ApplicationIds, appA.Id) || !slices.Contains(body.ApplicationIds, appB.Id) { + t.Fatalf("response applicationIds missing expected ids: %v", body.ApplicationIds) + } + + stored, err := gorm.G[models.Notification](db.DB).Preload("Applications", nil).Where("id = ?", body.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load stored notification: %v", err) + } + if stored.Config.String() != "discord://token@channel" { + t.Fatalf("expected config to roundtrip, got %q", stored.Config.String()) + } + if len(stored.Applications) != 2 { + t.Fatalf("expected 2 associated applications, got %d", len(stored.Applications)) + } +} + +func TestCreateNotificationHandler_InvalidRequest(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{}) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestCreateNotificationHandler_UnknownApplication(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Deploy Alerts", + "type": "discord", + "config": "discord://token@channel", + "applicationIds": []string{"missing-app"}, + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestCreateNotificationHandler_RejectsWhitespaceOnlyName(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": " \t ", + "type": "discord", + "config": "discord://token@channel", + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestCreateNotificationHandler_RejectsInvalidDirectTarget(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Deploy Alerts", + "type": "discord", + "config": "://", + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestCreateNotificationHandler_DiscordObjectConfigWithThreadID(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + appA := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Discord Alerts", + "type": "discord", + "config": map[string]any{ + "token": "token-abc", + "webhookId": "123456789", + "threadId": "987654321", + }, + "applicationIds": []string{appA.Id}, + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + + var body notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + stored, err := gorm.G[models.Notification](db.DB).Where("id = ?", body.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification: %v", err) + } + + urls, err := provider.BuildShouterrrUrls(stored.Type, stored.Config.String()) + if err != nil { + t.Fatalf("BuildShouterrrUrls() error: %v", err) + } + if len(urls) != 1 { + t.Fatalf("expected 1 URL, got %d", len(urls)) + } + if !strings.Contains(urls[0], "thread_id=987654321") { + t.Fatalf("expected built URL to include thread_id, got %s", urls[0]) + } +} + +func TestCreateNotificationHandler_InvalidDiscordObjectConfig(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Discord Alerts", + "type": "discord", + "config": map[string]any{ + "webhookId": "123456789", + }, + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestUpdateNotificationHandler_Success(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + appA := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + appB := seedNotificationApplication(t, repo.Id, agent.Id, "App B") + notification := createNotificationRecord(t, []string{appA.Id}) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Updated Alerts", + "enabled": false, + "enableByDefault": true, + "type": "discord", + "config": "discord://new-token@channel", + "applicationIds": []string{appB.Id}, + }) + + router := gin.New() + router.PUT("/api/v1/notifications/:id", UpdateNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/"+notification.Id, bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var body notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if body.Name != "Updated Alerts" { + t.Fatalf("expected name %q, got %q", "Updated Alerts", body.Name) + } + if body.Enabled { + t.Fatal("expected enabled=false") + } + if !body.EnableByDefault { + t.Fatal("expected enableByDefault=true") + } + if body.Status != string(models.NotificationStatusUnknown) { + t.Fatalf("expected status %q, got %q", models.NotificationStatusUnknown, body.Status) + } + if body.Config == nil || *body.Config != "discord://new-token@channel" { + t.Fatalf("expected updated config in update response, got %#v", body.Config) + } + if len(body.ApplicationIds) != 1 || body.ApplicationIds[0] != appB.Id { + t.Fatalf("expected applicationIds [%s], got %v", appB.Id, body.ApplicationIds) + } + + stored, err := gorm.G[models.Notification](db.DB).Preload("Applications", nil).Where("id = ?", notification.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load updated notification: %v", err) + } + if stored.Name.String() != "Updated Alerts" { + t.Fatalf("expected updated name, got %q", stored.Name.String()) + } + if stored.Enabled { + t.Fatal("expected stored enabled=false") + } + if !stored.EnableByDefault { + t.Fatal("expected stored enableByDefault=true") + } + if len(stored.Applications) != 1 || stored.Applications[0].Id != appB.Id { + t.Fatalf("expected stored application association to %s, got %+v", appB.Id, stored.Applications) + } +} + +func TestUpdateNotificationHandler_NotFound(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Updated Alerts", + "type": "discord", + "config": "discord://token@channel", + }) + + router := gin.New() + router.PUT("/api/v1/notifications/:id", UpdateNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/missing", bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestUpdateNotificationHandler_RejectsWhitespaceOnlyName(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + reqBody, _ := json.Marshal(map[string]any{ + "name": " ", + "type": "discord", + "config": "discord://token@channel", + }) + + router := gin.New() + router.PUT("/api/v1/notifications/:id", UpdateNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/"+notification.Id, bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestUpdateNotificationHandler_RejectsInvalidDirectTarget(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Updated Alerts", + "type": "discord", + "config": "://", + }) + + router := gin.New() + router.PUT("/api/v1/notifications/:id", UpdateNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/"+notification.Id, bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestDeleteNotificationHandler_Success(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + router := gin.New() + router.DELETE("/api/v1/notifications/:id", DeleteNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, "/api/v1/notifications/"+notification.Id, nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + _, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).First(t.Context()) + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected deleted notification to be missing, got err=%v", err) + } +} + +func TestDeleteNotificationHandler_NotFound(t *testing.T) { + setupTestDBWithNotifications(t) + + router := gin.New() + router.DELETE("/api/v1/notifications/:id", DeleteNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, "/api/v1/notifications/missing", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestTestNotificationHandler_Success(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + + called := false + sendTestNotification = func(notificationType models.NotificationType, rawConfig, message string) error { + called = true + if notificationType != models.NotificationTypeDiscord { + t.Fatalf("expected discord notification type, got %q", notificationType) + } + if rawConfig == "" { + t.Fatal("expected non-empty notification config") + } + if message != "ping" { + t.Fatalf("expected message %q, got %q", "ping", message) + } + + return nil + } + + reqBody, _ := json.Marshal(map[string]any{"message": "ping"}) + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if !called { + t.Fatal("expected test notification sender to be called") + } + + var body map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if body["message"] != "test notification sent" { + t.Fatalf("expected success message, got %q", body["message"]) + } + + stored, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification after test send: %v", err) + } + if stored.Status != models.NotificationStatusSuccess { + t.Fatalf("expected status %q after successful test send, got %q", models.NotificationStatusSuccess, stored.Status) + } +} + +func TestTestNotificationHandler_BindsChunkedJSONBody(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + + called := false + sendTestNotification = func(notificationType models.NotificationType, rawConfig, message string) error { + called = true + if message != "chunked-ping" { + t.Fatalf("expected message %q, got %q", "chunked-ping", message) + } + return nil + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + body := io.LimitReader(strings.NewReader(`{"message":"chunked-ping"}`), 1<<20) + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", body) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if !called { + t.Fatal("expected test notification sender to be called") + } +} + +func TestTestNotificationHandler_NotFound(t *testing.T) { + setupTestDBWithNotifications(t) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + t.Fatal("did not expect sender to be called") + return nil + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/missing/test", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestTestNotificationHandler_InvalidRequest(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + t.Fatal("did not expect sender to be called") + return nil + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", strings.NewReader(`{"message":123}`)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestTestNotificationHandler_DispatchFailure(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + return hubnotifications.ErrNotificationDispatch + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadGateway { + t.Fatalf("expected 502, got %d: %s", w.Code, w.Body.String()) + } + + stored, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification after test send failure: %v", err) + } + if stored.Status != models.NotificationStatusError { + t.Fatalf("expected status %q after failed test send, got %q", models.NotificationStatusError, stored.Status) + } +} + +func TestCreateNotificationHandler_DefaultFlagsWhenOmitted(t *testing.T) { + setupTestDBWithNotifications(t) + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Defaults", + "type": "discord", + "config": "discord://token@channel", + }) + + c, w := makeAuthContext(t, "user-1") + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/notifications", bytes.NewReader(reqBody)) + c.Request.Header.Set("Content-Type", "application/json") + + CreateNotificationHandler(c) + + if w.Code != http.StatusCreated { + t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + + var body notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if !body.Enabled { + t.Fatal("expected enabled default to true") + } + if body.EnableByDefault { + t.Fatal("expected enableByDefault default to false") + } +} + +func TestUpdateNotificationHandler_PreservesFlagsWhenOmitted(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + if err := db.DB.Model(&models.Notification{}). + Where("id = ?", notification.Id). + Updates(map[string]any{"enabled": false, "enable_by_default": true}).Error; err != nil { + t.Fatalf("failed to set initial notification flags: %v", err) + } + + reqBody, _ := json.Marshal(map[string]any{ + "name": "Updated Name", + "type": "discord", + "config": "discord://new-token@channel", + }) + + router := gin.New() + router.PUT("/api/v1/notifications/:id", UpdateNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/"+notification.Id, bytes.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var body notificationResponse + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + if body.Enabled { + t.Fatal("expected enabled to preserve existing false value") + } + if !body.EnableByDefault { + t.Fatal("expected enableByDefault to preserve existing true value") + } +} + +func TestTestNotificationHandler_InvalidConfigFailure(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + return fmt.Errorf("%w: invalid discord payload", hubnotifications.ErrInvalidNotificationConfig) + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } + + stored, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification after invalid-config send failure: %v", err) + } + if stored.Status != models.NotificationStatusError { + t.Fatalf("expected status %q after invalid-config send failure, got %q", models.NotificationStatusError, stored.Status) + } +} + +func TestTestNotificationHandler_UnexpectedFailure(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + return errors.New("boom") + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) + } + + stored, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).First(t.Context()) + if err != nil { + t.Fatalf("failed to load notification after unexpected send failure: %v", err) + } + if stored.Status != models.NotificationStatusError { + t.Fatalf("expected status %q after unexpected send failure, got %q", models.NotificationStatusError, stored.Status) + } +} + +func TestTestNotificationHandler_StatusUpdateNotFound(t *testing.T) { + setupTestDBWithNotifications(t) + + notification := createNotificationRecord(t, nil) + + originalSend := sendTestNotification + t.Cleanup(func() { + sendTestNotification = originalSend + }) + sendTestNotification = func(models.NotificationType, string, string) error { + rowsAffected, err := gorm.G[models.Notification](db.DB).Where("id = ?", notification.Id).Delete(t.Context()) + if err != nil { + t.Fatalf("failed to delete notification in test sender: %v", err) + } + if rowsAffected != 1 { + t.Fatalf("expected one notification row to be deleted, got %d", rowsAffected) + } + return nil + } + + router := gin.New() + router.POST("/api/v1/notifications/:id/test", TestNotificationHandler) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/notifications/"+notification.Id+"/test", nil) + router.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestNormalizeNotificationName_TooLong(t *testing.T) { + _, err := normalizeNotificationName(strings.Repeat("a", 129)) + if err == nil { + t.Fatal("expected error for name length over 128") + } +} + +func TestNormalizeNotificationConfig(t *testing.T) { + tests := []struct { + name string + raw json.RawMessage + want string + wantErr string + }{ + { + name: "empty", + raw: json.RawMessage(" "), + wantErr: "must be a non-empty string or JSON object", + }, + { + name: "null", + raw: json.RawMessage("null"), + wantErr: "must be a non-empty string or JSON object", + }, + { + name: "invalid json string", + raw: json.RawMessage("\"unterminated"), + wantErr: "expected a valid JSON string", + }, + { + name: "empty json string", + raw: json.RawMessage(`" "`), + wantErr: "must not be empty", + }, + { + name: "invalid json", + raw: json.RawMessage("{"), + wantErr: "expected valid JSON", + }, + { + name: "json string", + raw: json.RawMessage(`" discord://token@channel "`), + want: "discord://token@channel", + }, + { + name: "json object", + raw: json.RawMessage(`{"token":"token-abc","webhookId":"123456789"}`), + want: `{"token":"token-abc","webhookId":"123456789"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normalizeNotificationConfig(tt.raw) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("normalizeNotificationConfig() error = %v", err) + } + if got != tt.want { + t.Fatalf("expected %q, got %q", tt.want, got) + } + }) + } +} + +func TestNormalizeNotificationApplicationIds(t *testing.T) { + normalized := normalizeNotificationApplicationIds([]string{" app-a ", "", "app-a", "app-b", " app-b "}) + if !slices.Equal(normalized, []string{"app-a", "app-b"}) { + t.Fatalf("expected [app-a app-b], got %v", normalized) + } + + emptyNormalized := normalizeNotificationApplicationIds(nil) + if emptyNormalized != nil { + t.Fatalf("expected nil for empty input, got %v", emptyNormalized) + } +} + +func TestLoadNotificationApplications(t *testing.T) { + setupTestDBWithNotifications(t) + + repo := seedNotificationRepository(t) + agent := seedNotificationAgent(t) + app := seedNotificationApplication(t, repo.Id, agent.Id, "App A") + + apps, missingId, err := loadNotificationApplications(t.Context(), nil) + if err != nil { + t.Fatalf("loadNotificationApplications() empty input error = %v", err) + } + if len(apps) != 0 || missingId != "" { + t.Fatalf("expected empty result for empty input, got apps=%v missingId=%q", apps, missingId) + } + + _, missingId, err = loadNotificationApplications(t.Context(), []string{app.Id, "missing-app"}) + if err != nil { + t.Fatalf("loadNotificationApplications() missing id error = %v", err) + } + if missingId != "missing-app" { + t.Fatalf("expected missing id %q, got %q", "missing-app", missingId) + } +} + +func TestUpdateNotificationStatus_NotFound(t *testing.T) { + setupTestDBWithNotifications(t) + + err := updateNotificationStatus(t.Context(), "missing-id", models.NotificationStatusSuccess) + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Fatalf("expected gorm.ErrRecordNotFound, got %v", err) + } +} diff --git a/backend/internal/hub/websocket/hub_test.go b/backend/internal/hub/websocket/hub_test.go index 448e704..ab897af 100644 --- a/backend/internal/hub/websocket/hub_test.go +++ b/backend/internal/hub/websocket/hub_test.go @@ -26,6 +26,7 @@ func TestNewHub(t *testing.T) { if h == nil { t.Fatal("expected non-nil hub") + return } if h.clients == nil { t.Fatal("expected non-nil clients map") @@ -49,6 +50,7 @@ func TestHub_Register(t *testing.T) { if client == nil { t.Fatal("expected non-nil client") + return } if client.Id != "agent-1" { t.Errorf("expected client id %q, got %q", "agent-1", client.Id) @@ -145,6 +147,7 @@ func TestHub_Send_ExistingClient(t *testing.T) { ping := received.GetPing() if ping == nil { t.Fatal("expected ping payload") + return } if ping.Timestamp != 1234 { t.Errorf("expected timestamp 1234, got %d", ping.Timestamp) diff --git a/backend/internal/hub/websocket/worker_test.go b/backend/internal/hub/websocket/worker_test.go index fda8b25..14794db 100644 --- a/backend/internal/hub/websocket/worker_test.go +++ b/backend/internal/hub/websocket/worker_test.go @@ -16,6 +16,7 @@ func TestNewWorker(t *testing.T) { if w == nil { t.Fatal("expected non-nil worker") + return } if w.hub != h { t.Error("expected worker hub to match") @@ -47,6 +48,7 @@ func TestWorker_Start_BroadcastsPing(t *testing.T) { ping := msg.GetPing() if ping == nil { t.Fatal("expected ping payload") + return } if ping.Timestamp == 0 { t.Error("expected non-zero timestamp") diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 10efefa..f75e019 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -57,6 +57,7 @@ "searchUsers": "Benutzer suchen...", "searchApplications": "Anwendungen suchen...", "searchAgents": "Agenten suchen...", + "searchNotifications": "Benachrichtigungen suchen...", "noResults": "Keine Ergebnisse.", "noApplicationsFound": "Keine Anwendungen gefunden", "noApplicationsFoundDescription": "Passe deine Suche an, um passende Anwendungen zu sehen.", @@ -64,8 +65,11 @@ "noAgentsFoundDescription": "Passe deine Suche an, um passende Agenten zu sehen.", "noRepositoriesFound": "Keine Repositories gefunden", "noRepositoriesFoundDescription": "Passe deine Suche an, um passende Repositories zu sehen.", + "noNotificationsFound": "Keine Benachrichtigungen gefunden", + "noNotificationsFoundDescription": "Passe deine Suche an, um passende Benachrichtigungen zu sehen.", "totalAgentsCount": "Es gibt insgesamt {count} Agenten", "totalRepositoriesCount": "Es gibt insgesamt {count} Repositories", + "totalNotificationsCount": "Es gibt insgesamt {count} Benachrichtigungen", "appsCount": "Anzahl Apps", "notAvailableShort": "k. A.", "cardActions": "Kartenaktionen", @@ -73,6 +77,8 @@ "deleteAgentCardDescription": "Dadurch wird \"{name}\" dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", "deleteRepositoryTitle": "Repository löschen?", "deleteRepositoryDescription": "Dadurch wird \"{name}\" dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteNotificationTitle": "Benachrichtigung löschen?", + "deleteNotificationDescription": "Dadurch wird \"{name}\" dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", "statusOnline": "Online", "statusOffline": "Offline", "statusError": "Fehler", @@ -86,6 +92,7 @@ "navApplications": "Anwendungen", "navAgents": "Agenten", "navRepositories": "Repositories", + "navNotifications": "Benachrichtigungen", "navAdmin": "Admin", "hotkeysDialogTitle": "Tastenkürzel", "hotkeysDialogDescription": "Navigiere schneller mit diesen Kürzeln.", @@ -127,6 +134,9 @@ "pageRepositories": "Repositories", "repositoriesPageDescription": "Verbundene Git-Repositories verwalten", "loadingRepositories": "Repositories werden geladen...", + "pageNotifications": "Benachrichtigungen", + "notificationsPageDescription": "Verwalte Benachrichtigungsendpunkte und teste die Zustellung.", + "loadingNotifications": "Benachrichtigungen werden geladen...", "pageAgents": "Agenten", "agentsPageDescription": "Docker-Hosts und Server verwalten", "loadingAgents": "Agenten werden geladen...", @@ -217,6 +227,7 @@ "selectRepository": "Repository auswählen", "selectAgent": "Agent auswählen", "branch": "Branch", + "selectType": "Typ auswählen", "selectBranch": "Branch auswählen", "selectRepositoryFirst": "Zuerst ein Repository auswählen", "selectBranchFirst": "Zuerst einen Branch auswählen", @@ -232,6 +243,29 @@ "applicationCreated": "Anwendung erstellt", "failedSaveApplication": "Anwendung konnte nicht gespeichert werden", "updateApplication": "Anwendung aktualisieren", + "addNotification": "Benachrichtigung hinzufügen", + "editNotification": "Benachrichtigung bearbeiten", + "addNotificationDescription": "Erstelle einen neuen Benachrichtigungsendpunkt.", + "editNotificationDescription": "Aktualisiere die Benachrichtigungskonfiguration.", + "notificationNamePlaceholder": "z. B. \"deploy-alerts\"", + "notificationConfig": "Konfiguration", + "notificationConfigPlaceholder": "discord://token@webhook oder JSON-Objekt", + "notificationWebhookUrlPlaceholder": "https://discord.com/api/webhooks/123456789012345678/dein-webhook-token", + "notificationBotName": "Bot-Name", + "notificationBotNamePlaceholder": "z. B. \"Orca Bot\"", + "notificationAvatarUrl": "Avatar-URL", + "notificationAvatarUrlPlaceholder": "https://example.com/avatar.png", + "notificationThreadId": "Thread-ID", + "notificationThreadIdPlaceholder": "z. B. \"987654321012345678\"", + "notificationEnabledDescription": "Sende Benachrichtigungen, wenn Ereignisse ausgelöst werden.", + "enableByDefault": "Standardmäßig aktivieren", + "enableByDefaultDescription": "Wende diese Benachrichtigung automatisch auf neue Anwendungen an.", + "noApplicationsAvailable": "Keine Anwendungen verfügbar.", + "selectApplications": "Anwendungen auswählen...", + "notificationUpdated": "Benachrichtigung aktualisiert", + "notificationCreated": "Benachrichtigung erstellt", + "failedSaveNotification": "Benachrichtigung konnte nicht gespeichert werden", + "updateNotification": "Benachrichtigung aktualisieren", "validationApplicationNameRequired": "Name ist erforderlich", "validationApplicationNameMaxLength": "Name darf maximal 128 Zeichen lang sein", "validationRepositoryRequired": "Repository ist erforderlich", @@ -241,6 +275,13 @@ "validationPathRequired": "Pfad ist erforderlich", "validationPathMaxLength": "Pfad darf maximal 512 Zeichen lang sein", "validationPathMustBeYAML": "Pfad muss auf .yml oder .yaml enden", + "validationNotificationNameRequired": "Name ist erforderlich", + "validationNotificationNameMaxLength": "Name darf maximal 128 Zeichen lang sein", + "validationNotificationConfigRequired": "Konfiguration ist erforderlich", + "validationNotificationWebhookUrlRequired": "Webhook-URL ist erforderlich", + "validationNotificationWebhookUrlInvalid": "Webhook-URL muss eine gültige Discord-Webhook-URL sein", + "validationNotificationAvatarUrlInvalid": "Avatar-URL muss eine gültige URL sein", + "validationNotificationThreadIdInvalid": "Thread-ID darf nur Ziffern enthalten", "editProvider": "Anbieter bearbeiten", "addProvider": "Anbieter hinzufügen", "addProviderDescription": "Konfiguriere einen neuen OpenID-Connect-Anbieter.", @@ -324,11 +365,17 @@ "you": "Du", "providerPassword": "Passwort", "repositoryDeleted": "Repository {name} erfolgreich gelöscht", + "notificationDeleted": "Benachrichtigung {name} erfolgreich gelöscht", "failedDeleteRepository": "Repository konnte nicht gelöscht werden", + "failedDeleteNotification": "Benachrichtigung konnte nicht gelöscht werden", "failedDeleteAgent": "Agent konnte nicht gelöscht werden", + "testNotificationSent": "Testbenachrichtigung gesendet", + "failedSendTestNotification": "Testbenachrichtigung konnte nicht gesendet werden", + "sendTest": "Test senden", "agentDeleted": "Agent {name} erfolgreich gelöscht", "columnName": "Name", "columnStatus": "Status", + "columnType": "Typ", "columnAppsCount": "Anzahl Apps", "columnLastSeen": "Zuletzt gesehen", "columnUpdatedAt": "Aktualisiert am", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 39ac7cb..6b82198 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -57,6 +57,7 @@ "searchUsers": "Search users...", "searchApplications": "Search applications...", "searchAgents": "Search agents...", + "searchNotifications": "Search notifications...", "noResults": "No results.", "noApplicationsFound": "No applications found", "noApplicationsFoundDescription": "Adjust your search to see matching applications.", @@ -64,8 +65,11 @@ "noAgentsFoundDescription": "Adjust your search to see matching agents.", "noRepositoriesFound": "No repositories found", "noRepositoriesFoundDescription": "Adjust your search to see matching repositories.", + "noNotificationsFound": "No notifications found", + "noNotificationsFoundDescription": "Adjust your search to see matching notifications.", "totalAgentsCount": "There are {count} agents in total", "totalRepositoriesCount": "There are {count} repositories in total", + "totalNotificationsCount": "There are {count} notifications in total", "appsCount": "Apps count", "notAvailableShort": "n/a", "cardActions": "Card actions", @@ -73,6 +77,8 @@ "deleteAgentCardDescription": "This will permanently delete \"{name}\". This action cannot be undone.", "deleteRepositoryTitle": "Delete repository?", "deleteRepositoryDescription": "This will permanently delete \"{name}\". This action cannot be undone.", + "deleteNotificationTitle": "Delete notification?", + "deleteNotificationDescription": "This will permanently delete \"{name}\". This action cannot be undone.", "statusOnline": "Online", "statusOffline": "Offline", "statusError": "Error", @@ -86,6 +92,7 @@ "navApplications": "Applications", "navAgents": "Agents", "navRepositories": "Repositories", + "navNotifications": "Notifications", "navAdmin": "Admin", "hotkeysDialogTitle": "Keyboard Shortcuts", "hotkeysDialogDescription": "Navigate quickly with these shortcuts.", @@ -127,6 +134,9 @@ "pageRepositories": "Repositories", "repositoriesPageDescription": "Manage connected Git repositories", "loadingRepositories": "Loading repositories...", + "pageNotifications": "Notifications", + "notificationsPageDescription": "Manage notification endpoints and test delivery.", + "loadingNotifications": "Loading notifications...", "pageAgents": "Agents", "agentsPageDescription": "Manage Docker hosts and servers", "loadingAgents": "Loading agents...", @@ -217,6 +227,7 @@ "selectRepository": "Select a repository", "selectAgent": "Select an agent", "branch": "Branch", + "selectType": "Select type", "selectBranch": "Select a branch", "selectRepositoryFirst": "Select a repository first", "selectBranchFirst": "Select a branch first", @@ -232,6 +243,29 @@ "applicationCreated": "Application created", "failedSaveApplication": "Failed to save application", "updateApplication": "Update Application", + "addNotification": "Add Notification", + "editNotification": "Edit Notification", + "addNotificationDescription": "Create a new notification endpoint.", + "editNotificationDescription": "Update the notification configuration.", + "notificationNamePlaceholder": "e.g. \"deploy-alerts\"", + "notificationConfig": "Config", + "notificationConfigPlaceholder": "discord://token@webhook or JSON object", + "notificationWebhookUrlPlaceholder": "https://discord.com/api/webhooks/123456789012345678/your-webhook-token", + "notificationBotName": "Bot Name", + "notificationBotNamePlaceholder": "e.g. \"Orca Bot\"", + "notificationAvatarUrl": "Avatar URL", + "notificationAvatarUrlPlaceholder": "https://example.com/avatar.png", + "notificationThreadId": "Thread ID", + "notificationThreadIdPlaceholder": "e.g. \"987654321012345678\"", + "notificationEnabledDescription": "Send notifications when events are emitted.", + "enableByDefault": "Enable By Default", + "enableByDefaultDescription": "Apply this notification automatically to new applications.", + "noApplicationsAvailable": "No applications available.", + "selectApplications": "Select applications...", + "notificationUpdated": "Notification updated", + "notificationCreated": "Notification created", + "failedSaveNotification": "Failed to save notification", + "updateNotification": "Update Notification", "validationApplicationNameRequired": "Name is required", "validationApplicationNameMaxLength": "Name must be at most 128 characters", "validationRepositoryRequired": "Repository is required", @@ -241,6 +275,13 @@ "validationPathRequired": "Path is required", "validationPathMaxLength": "Path must be at most 512 characters", "validationPathMustBeYAML": "Path must end with .yml or .yaml", + "validationNotificationNameRequired": "Name is required", + "validationNotificationNameMaxLength": "Name must be at most 128 characters", + "validationNotificationConfigRequired": "Config is required", + "validationNotificationWebhookUrlRequired": "Webhook URL is required", + "validationNotificationWebhookUrlInvalid": "Webhook URL must be a valid Discord webhook URL", + "validationNotificationAvatarUrlInvalid": "Avatar URL must be a valid URL", + "validationNotificationThreadIdInvalid": "Thread ID must contain only digits", "editProvider": "Edit Provider", "addProvider": "Add Provider", "addProviderDescription": "Configure a new OpenID Connect provider.", @@ -324,11 +365,17 @@ "you": "You", "providerPassword": "Password", "repositoryDeleted": "Repository {name} deleted successfully", + "notificationDeleted": "Notification {name} deleted successfully", "failedDeleteRepository": "Failed to delete repository", + "failedDeleteNotification": "Failed to delete notification", "failedDeleteAgent": "Failed to delete agent", + "testNotificationSent": "Test notification sent", + "failedSendTestNotification": "Failed to send test notification", + "sendTest": "Send Test", "agentDeleted": "Agent {name} deleted successfully", "columnName": "Name", "columnStatus": "Status", + "columnType": "Type", "columnAppsCount": "Apps Count", "columnLastSeen": "Last Seen", "columnUpdatedAt": "Updated At", diff --git a/frontend/package.json b/frontend/package.json index 08e14cd..37ed111 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,27 +13,28 @@ "translations:check": "inlang validate --project ./project.inlang" }, "dependencies": { - "@fontsource-variable/geist-mono": "^5.2.7", + "@base-ui/react": "^1.4.1", + "@fontsource-variable/geist-mono": "^5.2.8", "@stepperize/react": "^6.1.0", "@tailwindcss/vite": "^4.3.0", - "@tanstack/react-devtools": "^0.10.2", - "@tanstack/react-form": "^1.31.0", + "@tanstack/react-devtools": "^0.10.5", + "@tanstack/react-form": "^1.32.0", "@tanstack/react-hotkeys": "^0.10.0", - "@tanstack/react-router": "^1.169.2", - "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/react-router": "^1.170.4", + "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-table": "^8.21.3", - "@tanstack/router-plugin": "^1.167.35", + "@tanstack/router-plugin": "^1.168.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "diff": "^9.0.0", - "lucide-react": "^1.14.0", + "lucide-react": "^1.16.0", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", "shadcn": "^4.7.0", "sonner": "^2.0.7", "swr": "^2.4.1", - "tailwind-merge": "^3.5.0", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", "tw-animate-css": "^1.4.0", "vite-plugin-compression2": "^2.5.3", @@ -41,21 +42,21 @@ }, "devDependencies": { "@inlang/cli": "^3.1.11", - "@inlang/paraglide-js": "^2.18.0", + "@inlang/paraglide-js": "^2.18.1", "@inlang/plugin-m-function-matcher": "^2.2.6", "@inlang/plugin-message-format": "^4.4.0", - "@shikijs/transformers": "^4.0.2", - "@tanstack/devtools-vite": "^0.6.0", - "@types/node": "^25.6.2", - "@types/react": "^19.2.14", + "@shikijs/transformers": "^4.1.0", + "@tanstack/devtools-vite": "^0.7.0", + "@types/node": "^25.9.1", + "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "oxfmt": "^0.48.0", - "oxlint": "^1.63.0", - "oxlint-tsgolint": "^0.22.1", - "shiki": "^4.0.2", + "@vitejs/plugin-react": "^6.0.2", + "oxfmt": "^0.51.0", + "oxlint": "^1.66.0", + "oxlint-tsgolint": "^0.23.0", + "shiki": "^4.1.0", "typescript": "^6.0.3", - "vite": "^8.0.11" + "vite": "^8.0.13" }, "packageManager": "pnpm@11.1.1" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index a619fe2..0f9bece 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,36 +8,39 @@ importers: .: dependencies: + '@base-ui/react': + specifier: ^1.4.1 + version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@fontsource-variable/geist-mono': - specifier: ^5.2.7 - version: 5.2.7 + specifier: ^5.2.8 + version: 5.2.8 '@stepperize/react': specifier: ^6.1.0 version: 6.1.0(react@19.2.6)(typescript@6.0.3) '@tailwindcss/vite': specifier: ^4.3.0 - version: 4.3.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) + version: 4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) '@tanstack/react-devtools': - specifier: ^0.10.2 - version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.12) + specifier: ^0.10.5 + version: 0.10.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.12) '@tanstack/react-form': - specifier: ^1.31.0 - version: 1.31.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^1.32.0 + version: 1.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-hotkeys': specifier: ^0.10.0 version: 0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-router': - specifier: ^1.169.2 - version: 1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^1.170.4 + version: 1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-router-devtools': - specifier: ^1.166.13 - version: 1.166.13(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.169.2)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^1.167.0 + version: 1.167.0(@tanstack/react-router@1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.2)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/router-plugin': - specifier: ^1.167.35 - version: 1.167.35(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) + specifier: ^1.168.6 + version: 1.168.6(@tanstack/react-router@1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -48,11 +51,11 @@ importers: specifier: ^9.0.0 version: 9.0.0 lucide-react: - specifier: ^1.14.0 - version: 1.14.0(react@19.2.6) + specifier: ^1.16.0 + version: 1.16.0(react@19.2.6) radix-ui: specifier: ^1.4.3 - version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: ^19.2.6 version: 19.2.6 @@ -61,7 +64,7 @@ importers: version: 19.2.6(react@19.2.6) shadcn: specifier: ^4.7.0 - version: 4.7.0(@types/node@25.6.2)(typescript@6.0.3) + version: 4.7.0(@types/node@25.9.1)(typescript@6.0.3) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -69,8 +72,8 @@ importers: specifier: ^2.4.1 version: 2.4.1(react@19.2.6) tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 + specifier: ^3.6.0 + version: 3.6.0 tailwindcss: specifier: ^4.3.0 version: 4.3.0 @@ -88,8 +91,8 @@ importers: specifier: ^3.1.11 version: 3.1.11 '@inlang/paraglide-js': - specifier: ^2.18.0 - version: 2.18.0(typescript@6.0.3) + specifier: ^2.18.1 + version: 2.18.1(typescript@6.0.3) '@inlang/plugin-m-function-matcher': specifier: ^2.2.6 version: 2.2.6 @@ -97,41 +100,41 @@ importers: specifier: ^4.4.0 version: 4.4.0 '@shikijs/transformers': - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^4.1.0 + version: 4.1.0 '@tanstack/devtools-vite': - specifier: ^0.6.0 - version: 0.6.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) + specifier: ^0.7.0 + version: 0.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) '@types/node': - specifier: ^25.6.2 - version: 25.6.2 + specifier: ^25.9.1 + version: 25.9.1 '@types/react': - specifier: ^19.2.14 - version: 19.2.14 + specifier: ^19.2.15 + version: 19.2.15 '@types/react-dom': specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) + version: 19.2.3(@types/react@19.2.15) '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) + specifier: ^6.0.2 + version: 6.0.2(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)) oxfmt: - specifier: ^0.48.0 - version: 0.48.0 + specifier: ^0.51.0 + version: 0.51.0 oxlint: - specifier: ^1.63.0 - version: 1.63.0(oxlint-tsgolint@0.22.1) + specifier: ^1.66.0 + version: 1.66.0(oxlint-tsgolint@0.23.0) oxlint-tsgolint: - specifier: ^0.22.1 - version: 0.22.1 + specifier: ^0.23.0 + version: 0.23.0 shiki: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^4.1.0 + version: 4.1.0 typescript: specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.0.11 - version: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + specifier: ^8.0.13 + version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) packages: @@ -252,6 +255,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -264,6 +271,33 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.4.1': + resolution: {integrity: sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.2.8': + resolution: {integrity: sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@dotenvx/dotenvx@1.65.0': resolution: {integrity: sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg==} hasBin: true @@ -298,8 +332,8 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@fontsource-variable/geist-mono@5.2.7': - resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==} + '@fontsource-variable/geist-mono@5.2.8': + resolution: {integrity: sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA==} '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} @@ -312,8 +346,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@inlang/paraglide-js@2.18.0': - resolution: {integrity: sha512-Acp6htA5W7rS2kL3iMjhSr08eECz696MYsud+FBKrmahzM7PdywPVq9UOr9MaC/aV7AZPElrYcqYZOOlUri5fg==} + '@inlang/paraglide-js@2.18.1': + resolution: {integrity: sha512-YzIrJ34KbsJwzvVKBzcBEgp4AqmhNYD1EF2pOV+g87L024XTFmcmikI4/NMfb63H9zJ6UeEPTOmEK7Nzu/4E2Q==} hasBin: true peerDependencies: typescript: '>=5' @@ -448,279 +482,409 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/types@0.128.0': - resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + '@oxc-parser/binding-android-arm-eabi@0.120.0': + resolution: {integrity: sha512-WU3qtINx802wOl8RxAF1v0VvmC2O4D9M8Sv486nLeQ7iPHVmncYZrtBhB4SYyX+XZxj2PNnCcN+PW21jHgiOxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.120.0': + resolution: {integrity: sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.120.0': + resolution: {integrity: sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.120.0': + resolution: {integrity: sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.120.0': + resolution: {integrity: sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] - '@oxfmt/binding-android-arm-eabi@0.48.0': - resolution: {integrity: sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': + resolution: {integrity: sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': + resolution: {integrity: sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': + resolution: {integrity: sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.120.0': + resolution: {integrity: sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': + resolution: {integrity: sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': + resolution: {integrity: sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': + resolution: {integrity: sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': + resolution: {integrity: sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.120.0': + resolution: {integrity: sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.120.0': + resolution: {integrity: sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.120.0': + resolution: {integrity: sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.120.0': + resolution: {integrity: sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + resolution: {integrity: sha512-1T0HKGcsz/BKo77t7+89L8Qvu4f9DoleKWHp3C5sJEcbCjDOLx3m9m722bWZTY+hANlUEs+yjlK+lBFsA+vrVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + resolution: {integrity: sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + resolution: {integrity: sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + + '@oxc-project/types@0.130.0': + resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + resolution: {integrity: sha512-Ni0sCqg5CIHaLIYFGj+ncbcumylvNC6FE4rfD0KfdmnWHbPJ+zev0qZCXKxy2hFVa0fYRK0yPzf5nzPbkZou7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.48.0': - resolution: {integrity: sha512-VUCiKuXK5+McVssgHEJdrcGK7hRJzrRb36zm9/jwzMholyYt4BgXhw5Nm1V1DX6Ce717Zi/1jk432b/tgmQgtQ==} + '@oxfmt/binding-android-arm64@0.51.0': + resolution: {integrity: sha512-eu5lAZjuo0KAkp+M24EhDqfOwA8owQ8d7wyBlOUUGRbDLHpU3IRlDHp8Dif+YqGlxs6jra7yS6WQu/NkPhAxeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.48.0': - resolution: {integrity: sha512-IkKp8rnIyQLW6Jt+6jragCbUVYSayk55lapiprLjIVvt4NczLyO/nwX2GgefLQ5iaBdfS8UEAFgCs/pLO6Cl0w==} + '@oxfmt/binding-darwin-arm64@0.51.0': + resolution: {integrity: sha512-6LsUNIdURhhcIfIn8+xsOb61mSTa9msAHTeSGx9Jf4rsP/gN8PGCF+SKWPAQZbND2w/WBkqQ6303jqEEIXzMdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.48.0': - resolution: {integrity: sha512-+aFuhsGIuvnoOjXyKVHMhPKJZR1kQkAl8QyrKoMlA7yJsSTC3N0Asl53La8TChSHhW8epToQ/Q0nvLmEmfNmLg==} + '@oxfmt/binding-darwin-x64@0.51.0': + resolution: {integrity: sha512-9aUMGmVxdHjYMsEAW1tNRoieTJXlVNDFkRvIR1J7LttJXWjVYCu2ekclLij2KJtxBxSQOYSHd12ME/adVGVbZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.48.0': - resolution: {integrity: sha512-fbqzQL8FjI9gGnktI7RIo0dksDziTAYBy7xlI7jU7eID5fxLF/25fS4Xj6GydD8Y5oWHL83U4NK160QaOAxtyg==} + '@oxfmt/binding-freebsd-x64@0.51.0': + resolution: {integrity: sha512-mkY1nhZTqYb+NHaAWxOCKISN6FwdrwMNsu17vTUA3wzUV2VJ+Paq15ZokRcsMU/2PUdHO73prxyeJpjXQ3MPpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.48.0': - resolution: {integrity: sha512-hn4i0zhAyTiB3ZHjQfYUZkDvrbVkohw1S7pySWxWUoZ87HnkDoTFThj7QTxk40hNPOTUP0vHbPRNamFIv1HBJQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + resolution: {integrity: sha512-wtFwNwE4+YCNuPaWoGDZeGsKvD6D1YSUNBJNn/rJBh7CrDBThFE+TBI5kY7vRW9rIOQRsbW2IpyyL3Du4Zqwiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.48.0': - resolution: {integrity: sha512-R4WBD9qF3QM9hqgdAa+fBGXmquTvDUujrPQ36t2Sjk8RPOSKGHDeN7l/khr10hqbQaOq9KCgPHG9ubNET/X/RQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + resolution: {integrity: sha512-rnOaNx86G7iRKM6lsCIQMux0SMGNC/TEbFR+r7lpruJ12bnrIWgxd5w1PLqOvgR9r8ZJbpK/zfRKctJnh8/Jfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.48.0': - resolution: {integrity: sha512-5bVdwSwlm1M8wbYCorLOxWxUBw/8tBvHYyQNIfwWVPwOJaj5vg1APSGJQVpwJfV5VNE9PSrR91UKEpoNwHhqUA==} + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + resolution: {integrity: sha512-jOgDzSqWcICGRjsp4mc08FxKMN8vzP2Kgs4E0d2HUP99F+nJDQKklRV4Zuj+0gcBgjrzx2CbpqaIdUVPepCojA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.48.0': - resolution: {integrity: sha512-vCS3Fk7gFslTqE1lUE2IlroyVV7u/9SmMA/uBqDoshuck2psGWcjW0ePyPZI3rM3+qtf2pDaMVIKMHozraifuw==} + '@oxfmt/binding-linux-arm64-musl@0.51.0': + resolution: {integrity: sha512-KBUCdrH5bwVrAvI9gU/1S55oH6fzXjr++J/oVocdu7bYTks1l7DNNT+rLd/1TDdAEjObGwmfWamn7LC1m8A0DQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.48.0': - resolution: {integrity: sha512-gKtfFfueUClXDumyoHUbymqRf7prHejOOyzJK0eIJn93GF9JBdFHdo60TM1ZBHxkEwZvjuOgHmKtneKbEOc/Eg==} + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + resolution: {integrity: sha512-NapfjYsABFqTJ1Dn9Efq6sN5esaHconVKwVLbDGNQLrwpOx/g17mkwErHzU72PutL67nf3wNAkbq122H+zLxag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.48.0': - resolution: {integrity: sha512-SYt0UhOvZD/UwZz9sXq6J2uAw8o24f5VZpLB2DH01f6MevshmlgakQlZe2lwek2sZJkd07eLu7mZa0g7yeiw7Q==} + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + resolution: {integrity: sha512-5dlDt1dUZCVi6elIhiK1PWg9wpTzTcIuj0IZnSurvIoMrhOWqqTcc1dSTxcSkNaBZhfsNqRZdINI1zAgbKkJNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.48.0': - resolution: {integrity: sha512-JLbrwck2AopG4ud/XklZO5N+qxGC7cS7ROvXZVNfx0MCLDDL2kGOLvzuWORkVjnjAM0CMAfIMU2zNBtQbM+4dw==} + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + resolution: {integrity: sha512-pgdWUJn0S5nulyiVdlFV8DzCUnGXkU99W5PSkkmbaZW+LrZBPxpezun4G0DDHbQaVYuJeCuKsXsGKGo77CkUTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.48.0': - resolution: {integrity: sha512-mdxt5L8OQLxkQH+JVpdC/lknZNe0lX4hlO3d8+xvw2wToo+iDrid9tiGOd5bmHfUVd5wVhrUry0qlu5vq66NkQ==} + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + resolution: {integrity: sha512-2XTFUe97CbDGAI8vjwDfZ1HdakO0XIADyJ24idEg64SC4/K4in/OisXVnrW4NMK7I6TgC7EqRhC0Ln/nKhAemA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.48.0': - resolution: {integrity: sha512-oEz1BQwMrV7OMEFx/3VPDU3n9TM0AnxpktDYXjEg5i6nTX87wo18wSfBvkl4tzAICdKtoAQAdBIl7Y7hsPlx5w==} + '@oxfmt/binding-linux-x64-gnu@0.51.0': + resolution: {integrity: sha512-kQ1OuCqqt/yyf0ZN9VFxW1/JnlgJgii3Dr7pWf9vNBvrX1hv6g39/+mc5oGRHRGJFZtl3zsGDWR9c5N2B/gwBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.48.0': - resolution: {integrity: sha512-g2SKTTurP5mWjd8Ecait0erYqmltL4IqW1EwttM25BxM6NiTt4ubobJYMR1uox1V2QgG4UfHH10CGRvWlUixjw==} + '@oxfmt/binding-linux-x64-musl@0.51.0': + resolution: {integrity: sha512-ARTYqxHF475o96Gbn41hvSWSSRygPlRDXZZgZ9I2scU1y0qiWpCQyZCoefaQa0mwv+wwtZ+luS4YOzsRzM/izg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.48.0': - resolution: {integrity: sha512-CIg24VgheEpvolHL2gQuax5qcQ602bRMHrJ9g8XsQr3iVj9aSPgopigBKuMqrXsupwkrU+RQCn5cG8PgFntR6w==} + '@oxfmt/binding-openharmony-arm64@0.51.0': + resolution: {integrity: sha512-QiC1XrCl6a6BmqMzduO8hdIRMf1m44hCkt2Q68KWkTvUB/E7fd2iomyNh6KnnRca5w6eBrRAAtLFqTh+xjsjJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.48.0': - resolution: {integrity: sha512-zeaWkcxcEULwkGF3I/HgEvcDPN8buYDrxibBUa/IFh5Vmwyge+KpLO+hEwSovW349H0O/C0Z2kaFmEzEDm00/Q==} + '@oxfmt/binding-win32-arm64-msvc@0.51.0': + resolution: {integrity: sha512-NC/hJb9dtU23Zf8L7IVK95xnFjiQ7AfcLO2l5pb69TDEr958qxrtnB2CveeeNSCBFNIkgaTCfd/vHNSoG78l9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.48.0': - resolution: {integrity: sha512-yiEKnIAGvx5CyZQOlMaNlZkAbwT7/Quk0j3WLt+PR5hK+qYjPTRRJYDfD77wCBPLvEYAG41v4KG3iL0H+uxoxg==} + '@oxfmt/binding-win32-ia32-msvc@0.51.0': + resolution: {integrity: sha512-2C45za4Rj36n8YIbhRL1PQbxmXJYf81WEcAgvj5I4ptRROG+A+81hREEN5bmCHADE1UfYaN312U6tkILoZZy6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.48.0': - resolution: {integrity: sha512-GSD2+7t2UoVMV2NgxXypa4bKewflPMAjYnF0Xw9/ht82ZfafAHhb8STwrEd7wlH2PFogt5zw3WVCxYJaHUdbeQ==} + '@oxfmt/binding-win32-x64-msvc@0.51.0': + resolution: {integrity: sha512-73RqdAuVKQTkjZIDw08JaDHUM4lav5Qu+CaPwg4QbbA7k8o7LEW0p3UsfZ/F8dsO/pwVYh3RzFcanwLRTTahbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.22.1': - resolution: {integrity: sha512-4150Lpgc1YM09GcjA6GSrra1JoPjC7aOpfywLjWEY4vW0Sd1qKzqHF1WRaiw0/qUZ40OATYdv3aRd7ipPkWQbw==} + '@oxlint-tsgolint/darwin-arm64@0.23.0': + resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.22.1': - resolution: {integrity: sha512-vFWcPWYOgZs4HWcgS1EjUZg33NLcNfEYU49KGImmCfZWkflENrmBYV4HN/C0YeAPum6ZZ/goPSvQrB/cOD+NfA==} + '@oxlint-tsgolint/darwin-x64@0.23.0': + resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.22.1': - resolution: {integrity: sha512-6LiUpP0Zir3+29FvBm7Y28q/dBjSHqTZ5MhG1Ckw4fGhI4cAvbcwXaKvbjx1TP7rRmBNOoq/M5xdpHjTb+GAew==} + '@oxlint-tsgolint/linux-arm64@0.23.0': + resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.22.1': - resolution: {integrity: sha512-fuX1hEQfpHauUbXADsfqVhRzrUrGabzGXbj5wsp2vKhV5uk/Rze8Mba9GdjFGECzvXudMGqHqxB4r6jGRdhxVA==} + '@oxlint-tsgolint/linux-x64@0.23.0': + resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.22.1': - resolution: {integrity: sha512-8SZidAj+jrbZf9ZjBEYW0tiNZ+KasqB2zgW26qdiPpQSF/DzURnPmXz651IeA9YsmbVdHGIooEHUmev6QJdquA==} + '@oxlint-tsgolint/win32-arm64@0.23.0': + resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.22.1': - resolution: {integrity: sha512-QweSk9H5lFh5Y+WUf2Kq/OAN88V6+62ZwGhP38gqdRotI90luXSMkruFTj7Q2rYrzH4ZVNaSqx7NY8JpSfIzqg==} + '@oxlint-tsgolint/win32-x64@0.23.0': + resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.63.0': - resolution: {integrity: sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg==} + '@oxlint/binding-android-arm-eabi@1.66.0': + resolution: {integrity: sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.63.0': - resolution: {integrity: sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A==} + '@oxlint/binding-android-arm64@1.66.0': + resolution: {integrity: sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.63.0': - resolution: {integrity: sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g==} + '@oxlint/binding-darwin-arm64@1.66.0': + resolution: {integrity: sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.63.0': - resolution: {integrity: sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw==} + '@oxlint/binding-darwin-x64@1.66.0': + resolution: {integrity: sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.63.0': - resolution: {integrity: sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA==} + '@oxlint/binding-freebsd-x64@1.66.0': + resolution: {integrity: sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.63.0': - resolution: {integrity: sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w==} + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': + resolution: {integrity: sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.63.0': - resolution: {integrity: sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg==} + '@oxlint/binding-linux-arm-musleabihf@1.66.0': + resolution: {integrity: sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.63.0': - resolution: {integrity: sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ==} + '@oxlint/binding-linux-arm64-gnu@1.66.0': + resolution: {integrity: sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.63.0': - resolution: {integrity: sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g==} + '@oxlint/binding-linux-arm64-musl@1.66.0': + resolution: {integrity: sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.63.0': - resolution: {integrity: sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ==} + '@oxlint/binding-linux-ppc64-gnu@1.66.0': + resolution: {integrity: sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.63.0': - resolution: {integrity: sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA==} + '@oxlint/binding-linux-riscv64-gnu@1.66.0': + resolution: {integrity: sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.63.0': - resolution: {integrity: sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ==} + '@oxlint/binding-linux-riscv64-musl@1.66.0': + resolution: {integrity: sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.63.0': - resolution: {integrity: sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg==} + '@oxlint/binding-linux-s390x-gnu@1.66.0': + resolution: {integrity: sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.63.0': - resolution: {integrity: sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA==} + '@oxlint/binding-linux-x64-gnu@1.66.0': + resolution: {integrity: sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.63.0': - resolution: {integrity: sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A==} + '@oxlint/binding-linux-x64-musl@1.66.0': + resolution: {integrity: sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.63.0': - resolution: {integrity: sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w==} + '@oxlint/binding-openharmony-arm64@1.66.0': + resolution: {integrity: sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.63.0': - resolution: {integrity: sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg==} + '@oxlint/binding-win32-arm64-msvc@1.66.0': + resolution: {integrity: sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.63.0': - resolution: {integrity: sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w==} + '@oxlint/binding-win32-ia32-msvc@1.66.0': + resolution: {integrity: sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.63.0': - resolution: {integrity: sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA==} + '@oxlint/binding-win32-x64-msvc@1.66.0': + resolution: {integrity: sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1415,106 +1579,103 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rolldown/binding-android-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + '@rolldown/binding-android-arm64@1.0.1': + resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + '@rolldown/binding-darwin-arm64@1.0.1': + resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.18': - resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + '@rolldown/binding-darwin-x64@1.0.1': + resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': - resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + '@rolldown/binding-freebsd-x64@1.0.1': + resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': - resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.1': + resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + '@rolldown/binding-linux-arm64-musl@1.0.1': + resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.1': + resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + '@rolldown/binding-linux-s390x-gnu@1.0.1': + resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': - resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + '@rolldown/binding-linux-x64-gnu@1.0.1': + resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': - resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + '@rolldown/binding-linux-x64-musl@1.0.1': + resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': - resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + '@rolldown/binding-openharmony-arm64@1.0.1': + resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': - resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + '@rolldown/binding-wasm32-wasi@1.0.1': + resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.1': + resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': - resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + '@rolldown/binding-win32-x64-msvc@1.0.1': + resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.18': - resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} @@ -1528,36 +1689,36 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@shikijs/core@4.0.2': - resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + '@shikijs/core@4.1.0': + resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.0.2': - resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + '@shikijs/engine-javascript@4.1.0': + resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.0.2': - resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + '@shikijs/engine-oniguruma@4.1.0': + resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==} engines: {node: '>=20'} - '@shikijs/langs@4.0.2': - resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + '@shikijs/langs@4.1.0': + resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==} engines: {node: '>=20'} - '@shikijs/primitive@4.0.2': - resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + '@shikijs/primitive@4.1.0': + resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==} engines: {node: '>=20'} - '@shikijs/themes@4.0.2': - resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + '@shikijs/themes@4.1.0': + resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==} engines: {node: '>=20'} - '@shikijs/transformers@4.0.2': - resolution: {integrity: sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg==} + '@shikijs/transformers@4.1.0': + resolution: {integrity: sha512-YbuOcAA3kwqKDU9YSt00dtFLrY5lBXjKU3dWaMATyEyPSqBm9Jqblk/uVICxz7lcjwAHzYaEvIiMWX3mTpogkA==} engines: {node: '>=20'} - '@shikijs/types@4.0.2': - resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + '@shikijs/types@4.1.0': + resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -1721,31 +1882,31 @@ packages: engines: {node: '>=18'} hasBin: true - '@tanstack/devtools-ui@0.5.1': - resolution: {integrity: sha512-T9JjAdqMSnxsVO6AQykD5vhxPF4iFLKtbYxee/bU3OLlk446F5C1220GdCmhDSz7y4lx+m8AvIS0bq6zzvdDUA==} + '@tanstack/devtools-ui@0.5.2': + resolution: {integrity: sha512-GtaMk8kaGZ9ZdR8Pu5RAfcse/ZrxzH/xsAIFtHMapLs2VMqSPFfb1NvIDO1MAAfUcub8Ix8XKQEP0uYSPzoFKw==} engines: {node: '>=18'} peerDependencies: solid-js: '>=1.9.7' - '@tanstack/devtools-vite@0.6.0': - resolution: {integrity: sha512-h0r0ct7zlrgjkhmn4QW6wRjgUXd4JMs+r7gtx+BXo9f5H9Y+jtUdtvC0rnZcPto6gw/9yMUq7yOmMK5qDWRExg==} + '@tanstack/devtools-vite@0.7.0': + resolution: {integrity: sha512-VXki7K+Xwnpo3IKdNSWGe7YOvtZv33YlulGqaQ+YCpeQhYg8JFuxP50BXibDoRLj5EOX4r21Hs7COdxbRHXkTw==} engines: {node: '>=18'} hasBin: true peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@tanstack/devtools@0.11.2': - resolution: {integrity: sha512-K8+tsBx+ptTLqqd4dOF10B6laj1g+XYImqYZL9n0jBINGaT+sOf17PKV9pbBt8kdbZeIGsHaJ5OZWCyZoHqN4A==} + '@tanstack/devtools@0.12.2': + resolution: {integrity: sha512-Xdl8pLzoDUvXaclQ0poY36WAPx0jEHk8vqUFd8FYFUm1BMshtB7RnTgD1HE9jCAXODxqw9I0gXBiUZLK3o3+Bw==} engines: {node: '>=18'} hasBin: true peerDependencies: solid-js: '>=1.9.7' - '@tanstack/form-core@1.31.0': - resolution: {integrity: sha512-t5G/LnrM/U10mQgzYis/WvHc6yTDj7jyF5cYf6GjVJpEbn0et0wDUQtafiOXY8hiXJ9D1HUqBi0u76ZX05uiHw==} + '@tanstack/form-core@1.32.0': + resolution: {integrity: sha512-Tn5VRDSjyqjmaet2tJMuEWDRFyrCaon03vxXPlSSaiSs6C/N7lCIwGCXJbZXEUq1kTj8jYN9qyXHbsz4LQHcow==} - '@tanstack/history@1.161.6': - resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} + '@tanstack/history@1.162.0': + resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} '@tanstack/hotkeys@0.8.0': @@ -1756,8 +1917,8 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/react-devtools@0.10.2': - resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} + '@tanstack/react-devtools@0.10.5': + resolution: {integrity: sha512-orVsRJ7oAXFb7oyafQCgx9YuK44jpILh5T/ddYuxAsolNfN5DZBr5/NLrWErD7HCGIzvYzg1TZI4sPxmiKvtvA==} engines: {node: '>=18'} peerDependencies: '@types/react': '>=16.8' @@ -1765,8 +1926,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form@1.31.0': - resolution: {integrity: sha512-31hoeKlk45jCAnry85bOiL89FwySL9HtAhfWLHmCJ5WcvBlHJpoqnMvl6Cy8DM+7ey2eWHKiPiEL29BkG5o12w==} + '@tanstack/react-form@1.32.0': + resolution: {integrity: sha512-6WP5SQTA6/H9crCpvpq3ZppYWqtrdE5NjOy6ebABi6uAQPqhfTzrdjS9t40mCZCFtGI5585OhJV6zBP/KN2zcw==} peerDependencies: '@tanstack/react-start': '*' react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1781,20 +1942,20 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-router-devtools@1.166.13': - resolution: {integrity: sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==} + '@tanstack/react-router-devtools@1.167.0': + resolution: {integrity: sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/react-router': ^1.168.15 - '@tanstack/router-core': ^1.168.11 + '@tanstack/react-router': ^1.170.0 + '@tanstack/router-core': ^1.170.0 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.169.2': - resolution: {integrity: sha512-OJM7Kguc7ERnweaNRWsyWgIKcl3z23rD1B4jaxjzd9RGdnzpt2HfrWa9rggbT0Hfzhfo4D2ZmsfoTme035tniQ==} + '@tanstack/react-router@1.170.4': + resolution: {integrity: sha512-cusL4YCTuGGJhjfsXEBm6/SmOAs/G8wRVNadeyN3ofu4OZwX69KAybBEf217buxYzI+FohdJVoigEpJV+tGzIw==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1819,30 +1980,30 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/router-core@1.169.2': - resolution: {integrity: sha512-5sm0DJF1A7Mz+9gy4Gz/lLovNailK3yot4vYvz9MkBUPw26uLnhQiR8hSCYxucjE0wD6Mdlc5l+Z0/XTlZ7xHw==} + '@tanstack/router-core@1.171.2': + resolution: {integrity: sha512-sUd+BhGYkBF64LVhmOHnYsc1AutPNch/huohEXiXL4IUgmk17Gy+RkUazvjQhptVdYW5QT+qtATrUr2cQZNHFA==} engines: {node: '>=20.19'} - '@tanstack/router-devtools-core@1.167.3': - resolution: {integrity: sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==} + '@tanstack/router-devtools-core@1.168.0': + resolution: {integrity: sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.168.11 + '@tanstack/router-core': ^1.170.0 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.166.42': - resolution: {integrity: sha512-2qBWC0t78r6b3vI+AbnvCZcFAvbYBDlLuWZrTjQbcjUmwG3qyeQp983tJyDuj9wb5//adG1tgAGXZkJ3aDwdBg==} + '@tanstack/router-generator@1.167.5': + resolution: {integrity: sha512-S7h9qs7WjwF1IlMiOxSv+xB/bSOQ6QS84NlApM9iWLVdkbOVUn7RzTaCqw2qdDa5cPrfSiZJ2wK2a6RFDmFubA==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.167.35': - resolution: {integrity: sha512-UAScU5VAzLYVY4FML/Cbc5S5TucT4I8Ata05yozGOe4ZfepTKRffA5xWLtD2N+ov5svdv0KTX/kqlZnYPe28mA==} + '@tanstack/router-plugin@1.168.6': + resolution: {integrity: sha512-u5CNtTWGyFvV8gGWKBt9LdwVGg+ISSBXG/aeeU1/d1YpEKPqlJHS6oN3tvNKOScubeV64HjpeV0tD6fqRfCpvw==} engines: {node: '>=20.19'} peerDependencies: '@rsbuild/core': '>=1.0.2 || ^2.0.0' - '@tanstack/react-router': ^1.169.2 + '@tanstack/react-router': ^1.170.4 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' vite-plugin-solid: ^2.11.10 || ^3.0.0-0 webpack: '>=5.92.0' @@ -1858,8 +2019,8 @@ packages: webpack: optional: true - '@tanstack/router-utils@1.161.8': - resolution: {integrity: sha512-xyiLWEKjfBAVhauDSSjXxyf7s8elU6SM+V050sbkofvGmIIvkwPFtDsX7Gvwh14kBd6iCwAT+RiPvXTxAptY0Q==} + '@tanstack/router-utils@1.162.0': + resolution: {integrity: sha512-c3GhqhBRCP636B41nf3TKvVz8EWzC5PTZ3I4J4LDH2tVjpxbyFNYsQKRtbNWiMFl+GTtgK4nCha346Wv7j4hcQ==} engines: {node: '>=20.19'} '@tanstack/store@0.11.0': @@ -1872,10 +2033,9 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-file-routes@1.161.7': - resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} engines: {node: '>=20.19'} - hasBin: true '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} @@ -1892,16 +2052,16 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/node@25.6.2': - resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -1918,8 +2078,8 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 @@ -1967,8 +2127,8 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.3.0: + resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} engines: {node: '>=14'} anymatch@3.1.3: @@ -2459,8 +2619,8 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - goober@2.1.18: - resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + goober@2.1.19: + resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} peerDependencies: csstype: ^3.0.10 @@ -2770,8 +2930,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.14.0: - resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} + lucide-react@1.16.0: + resolution: {integrity: sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2936,17 +3096,26 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - oxfmt@0.48.0: - resolution: {integrity: sha512-AVaLh+7XeGx+R1zfFV+f6VV61nT2MWVJXVUDhbTm5LBWGyNt64xAyh3NYYyjeY2WykNt9AvqSQLPHcbWquYF9g==} + oxc-parser@0.120.0: + resolution: {integrity: sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxfmt@0.51.0: + resolution: {integrity: sha512-l/AoAnaEOV7Q5/Z9kHOMDehVJnCgYN7wRoooWCTUMBMi16BJhLZqd9cmCnwcVFfVlzkt53zK2KLPFNp8vSsoDg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + peerDependencies: + svelte: ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true - oxlint-tsgolint@0.22.1: - resolution: {integrity: sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg==} + oxlint-tsgolint@0.23.0: + resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} hasBin: true - oxlint@1.63.0: - resolution: {integrity: sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg==} + oxlint@1.66.0: + resolution: {integrity: sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3014,6 +3183,10 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} @@ -3130,6 +3303,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3145,8 +3321,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.18: - resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + rolldown@1.0.1: + resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3211,8 +3387,8 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} - shiki@4.0.2: - resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + shiki@4.1.0: + resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} engines: {node: '>=20'} side-channel-list@1.0.1: @@ -3321,8 +3497,8 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} @@ -3393,8 +3569,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} @@ -3492,8 +3668,8 @@ packages: vite-plugin-compression2@2.5.3: resolution: {integrity: sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==} - vite@8.0.11: - resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + vite@8.0.13: + resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3559,8 +3735,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3777,6 +3953,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.2': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -3800,6 +3978,29 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@base-ui/react@1.4.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.8(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@floating-ui/utils': 0.2.11 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + + '@base-ui/utils@0.2.8(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + '@dotenvx/dotenvx@1.65.0': dependencies: commander: 11.1.0 @@ -3850,7 +4051,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@fontsource-variable/geist-mono@5.2.7': {} + '@fontsource-variable/geist-mono@5.2.8': {} '@hono/node-server@1.19.14(hono@4.12.18)': dependencies: @@ -3863,7 +4064,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@inlang/paraglide-js@2.18.0(typescript@6.0.3)': + '@inlang/paraglide-js@2.18.1(typescript@6.0.3)': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.9.3 @@ -3903,30 +4104,30 @@ snapshots: '@inquirer/ansi@2.0.5': {} - '@inquirer/confirm@6.0.12(@types/node@25.6.2)': + '@inquirer/confirm@6.0.12(@types/node@25.9.1)': dependencies: - '@inquirer/core': 11.1.9(@types/node@25.6.2) - '@inquirer/type': 4.0.5(@types/node@25.6.2) + '@inquirer/core': 11.1.9(@types/node@25.9.1) + '@inquirer/type': 4.0.5(@types/node@25.9.1) optionalDependencies: - '@types/node': 25.6.2 + '@types/node': 25.9.1 - '@inquirer/core@11.1.9(@types/node@25.6.2)': + '@inquirer/core@11.1.9(@types/node@25.9.1)': dependencies: '@inquirer/ansi': 2.0.5 '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@25.6.2) + '@inquirer/type': 4.0.5(@types/node@25.9.1) cli-width: 4.1.0 fast-wrap-ansi: 0.2.0 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 25.6.2 + '@types/node': 25.9.1 '@inquirer/figures@2.0.5': {} - '@inquirer/type@4.0.5(@types/node@25.6.2)': + '@inquirer/type@4.0.5(@types/node@25.9.1)': optionalDependencies: - '@types/node': 25.6.2 + '@types/node': 25.9.1 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4030,939 +4231,1004 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/types@0.128.0': {} + '@oxc-parser/binding-android-arm-eabi@0.120.0': + optional: true - '@oxfmt/binding-android-arm-eabi@0.48.0': + '@oxc-parser/binding-android-arm64@0.120.0': optional: true - '@oxfmt/binding-android-arm64@0.48.0': + '@oxc-parser/binding-darwin-arm64@0.120.0': optional: true - '@oxfmt/binding-darwin-arm64@0.48.0': + '@oxc-parser/binding-darwin-x64@0.120.0': optional: true - '@oxfmt/binding-darwin-x64@0.48.0': + '@oxc-parser/binding-freebsd-x64@0.120.0': optional: true - '@oxfmt/binding-freebsd-x64@0.48.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.48.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.48.0': + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.48.0': + '@oxc-parser/binding-linux-arm64-musl@0.120.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.48.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.48.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.48.0': + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.48.0': + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.48.0': + '@oxc-parser/binding-linux-x64-gnu@0.120.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.48.0': + '@oxc-parser/binding-linux-x64-musl@0.120.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.48.0': + '@oxc-parser/binding-openharmony-arm64@0.120.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.48.0': + '@oxc-parser/binding-wasm32-wasi@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.120.0': + optional: true + + '@oxc-project/types@0.120.0': {} + + '@oxc-project/types@0.130.0': {} + + '@oxfmt/binding-android-arm-eabi@0.51.0': + optional: true + + '@oxfmt/binding-android-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.51.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.51.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.51.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.51.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.51.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.48.0': + '@oxfmt/binding-openharmony-arm64@0.51.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.48.0': + '@oxfmt/binding-win32-arm64-msvc@0.51.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.48.0': + '@oxfmt/binding-win32-ia32-msvc@0.51.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.22.1': + '@oxfmt/binding-win32-x64-msvc@0.51.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.22.1': + '@oxlint-tsgolint/darwin-arm64@0.23.0': optional: true - '@oxlint-tsgolint/linux-arm64@0.22.1': + '@oxlint-tsgolint/darwin-x64@0.23.0': optional: true - '@oxlint-tsgolint/linux-x64@0.22.1': + '@oxlint-tsgolint/linux-arm64@0.23.0': optional: true - '@oxlint-tsgolint/win32-arm64@0.22.1': + '@oxlint-tsgolint/linux-x64@0.23.0': optional: true - '@oxlint-tsgolint/win32-x64@0.22.1': + '@oxlint-tsgolint/win32-arm64@0.23.0': optional: true - '@oxlint/binding-android-arm-eabi@1.63.0': + '@oxlint-tsgolint/win32-x64@0.23.0': optional: true - '@oxlint/binding-android-arm64@1.63.0': + '@oxlint/binding-android-arm-eabi@1.66.0': optional: true - '@oxlint/binding-darwin-arm64@1.63.0': + '@oxlint/binding-android-arm64@1.66.0': optional: true - '@oxlint/binding-darwin-x64@1.63.0': + '@oxlint/binding-darwin-arm64@1.66.0': optional: true - '@oxlint/binding-freebsd-x64@1.63.0': + '@oxlint/binding-darwin-x64@1.66.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.63.0': + '@oxlint/binding-freebsd-x64@1.66.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.63.0': + '@oxlint/binding-linux-arm-gnueabihf@1.66.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.63.0': + '@oxlint/binding-linux-arm-musleabihf@1.66.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.63.0': + '@oxlint/binding-linux-arm64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.63.0': + '@oxlint/binding-linux-arm64-musl@1.66.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.63.0': + '@oxlint/binding-linux-ppc64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.63.0': + '@oxlint/binding-linux-riscv64-gnu@1.66.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.63.0': + '@oxlint/binding-linux-riscv64-musl@1.66.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.63.0': + '@oxlint/binding-linux-s390x-gnu@1.66.0': optional: true - '@oxlint/binding-linux-x64-musl@1.63.0': + '@oxlint/binding-linux-x64-gnu@1.66.0': optional: true - '@oxlint/binding-openharmony-arm64@1.63.0': + '@oxlint/binding-linux-x64-musl@1.66.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.63.0': + '@oxlint/binding-openharmony-arm64@1.66.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.63.0': + '@oxlint/binding-win32-arm64-msvc@1.66.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.63.0': + '@oxlint/binding-win32-ia32-msvc@1.66.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.66.0': optional: true '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) aria-hidden: 1.2.6 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) aria-hidden: 1.2.6 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) aria-hidden: 1.2.6 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@floating-ui/react-dom': 2.1.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) '@radix-ui/rect': 1.1.1 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) aria-hidden: 1.2.6 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll: 2.7.2(@types/react@19.2.15)(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: '@radix-ui/rect': 1.1.1 react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.6)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.15)(react@19.2.6)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) react: 19.2.6 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) '@radix-ui/rect@1.1.1': {} - '@rolldown/binding-android-arm64@1.0.0-rc.18': + '@rolldown/binding-android-arm64@1.0.1': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + '@rolldown/binding-darwin-arm64@1.0.1': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.18': + '@rolldown/binding-darwin-x64@1.0.1': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + '@rolldown/binding-freebsd-x64@1.0.1': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + '@rolldown/binding-linux-arm-gnueabihf@1.0.1': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-gnu@1.0.1': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-arm64-musl@1.0.1': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-ppc64-gnu@1.0.1': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-s390x-gnu@1.0.1': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + '@rolldown/binding-linux-x64-gnu@1.0.1': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + '@rolldown/binding-linux-x64-musl@1.0.1': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + '@rolldown/binding-openharmony-arm64@1.0.1': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + '@rolldown/binding-wasm32-wasi@1.0.1': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-arm64-msvc@1.0.1': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + '@rolldown/binding-win32-x64-msvc@1.0.1': optional: true - '@rolldown/pluginutils@1.0.0-rc.18': {} - - '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rolldown/pluginutils@1.0.1': {} '@rollup/pluginutils@5.3.0': dependencies: @@ -4972,45 +5238,45 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shikijs/core@4.0.2': + '@shikijs/core@4.1.0': dependencies: - '@shikijs/primitive': 4.0.2 - '@shikijs/types': 4.0.2 + '@shikijs/primitive': 4.1.0 + '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.0.2': + '@shikijs/engine-javascript@4.1.0': dependencies: - '@shikijs/types': 4.0.2 + '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.0.2': + '@shikijs/engine-oniguruma@4.1.0': dependencies: - '@shikijs/types': 4.0.2 + '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.0.2': + '@shikijs/langs@4.1.0': dependencies: - '@shikijs/types': 4.0.2 + '@shikijs/types': 4.1.0 - '@shikijs/primitive@4.0.2': + '@shikijs/primitive@4.1.0': dependencies: - '@shikijs/types': 4.0.2 + '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/themes@4.0.2': + '@shikijs/themes@4.1.0': dependencies: - '@shikijs/types': 4.0.2 + '@shikijs/types': 4.1.0 - '@shikijs/transformers@4.0.2': + '@shikijs/transformers@4.1.0': dependencies: - '@shikijs/core': 4.0.2 - '@shikijs/types': 4.0.2 + '@shikijs/core': 4.1.0 + '@shikijs/types': 4.1.0 - '@shikijs/types@4.0.2': + '@shikijs/types@4.1.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -5129,12 +5395,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': + '@tailwindcss/vite@4.3.0(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) '@tanstack/devtools-client@0.0.6': dependencies: @@ -5142,63 +5408,61 @@ snapshots: '@tanstack/devtools-event-bus@0.4.1': dependencies: - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate '@tanstack/devtools-event-client@0.4.3': {} - '@tanstack/devtools-ui@0.5.1(csstype@3.2.3)(solid-js@1.9.12)': + '@tanstack/devtools-ui@0.5.2(csstype@3.2.3)(solid-js@1.9.12)': dependencies: clsx: 2.1.1 dayjs: 1.11.20 - goober: 2.1.18(csstype@3.2.3) + goober: 2.1.19(csstype@3.2.3) solid-js: 1.9.12 transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.6.0(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': + '@tanstack/devtools-vite@0.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 '@tanstack/devtools-client': 0.0.6 '@tanstack/devtools-event-bus': 0.4.1 chalk: 5.6.2 launch-editor: 2.13.2 + magic-string: 0.30.21 + oxc-parser: 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) picomatch: 4.0.4 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - bufferutil - - supports-color - utf-8-validate - '@tanstack/devtools@0.11.2(csstype@3.2.3)(solid-js@1.9.12)': + '@tanstack/devtools@0.12.2(csstype@3.2.3)(solid-js@1.9.12)': dependencies: '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.12) '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.12) '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.12) '@tanstack/devtools-client': 0.0.6 '@tanstack/devtools-event-bus': 0.4.1 - '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.12) + '@tanstack/devtools-ui': 0.5.2(csstype@3.2.3)(solid-js@1.9.12) clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) + goober: 2.1.19(csstype@3.2.3) solid-js: 1.9.12 transitivePeerDependencies: - bufferutil - csstype - utf-8-validate - '@tanstack/form-core@1.31.0': + '@tanstack/form-core@1.32.0': dependencies: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.9.3 - '@tanstack/history@1.161.6': {} + '@tanstack/history@1.162.0': {} '@tanstack/hotkeys@0.8.0': dependencies: @@ -5206,11 +5470,11 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.12)': + '@tanstack/react-devtools@0.10.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.12)': dependencies: - '@tanstack/devtools': 0.11.2(csstype@3.2.3)(solid-js@1.9.12) - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@tanstack/devtools': 0.12.2(csstype@3.2.3)(solid-js@1.9.12) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: @@ -5219,9 +5483,9 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form@1.31.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-form@1.32.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/form-core': 1.31.0 + '@tanstack/form-core': 1.32.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 transitivePeerDependencies: @@ -5234,22 +5498,22 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.169.2)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-router-devtools@1.167.0(@tanstack/react-router@1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.2)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/react-router': 1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.169.2)(csstype@3.2.3) + '@tanstack/react-router': 1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@tanstack/router-devtools-core': 1.168.0(@tanstack/router-core@1.171.2)(csstype@3.2.3) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@tanstack/router-core': 1.169.2 + '@tanstack/router-core': 1.171.2 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-router@1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@tanstack/history': 1.161.6 + '@tanstack/history': 1.162.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tanstack/router-core': 1.169.2 + '@tanstack/router-core': 1.171.2 isbot: 5.1.40 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -5274,27 +5538,27 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - '@tanstack/router-core@1.169.2': + '@tanstack/router-core@1.171.2': dependencies: - '@tanstack/history': 1.161.6 + '@tanstack/history': 1.162.0 cookie-es: 3.1.1 seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) - '@tanstack/router-devtools-core@1.167.3(@tanstack/router-core@1.169.2)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.168.0(@tanstack/router-core@1.171.2)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.169.2 + '@tanstack/router-core': 1.171.2 clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) + goober: 2.1.19(csstype@3.2.3) optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.42': + '@tanstack/router-generator@1.167.5': dependencies: '@babel/types': 7.29.0 - '@tanstack/router-core': 1.169.2 - '@tanstack/router-utils': 1.161.8 - '@tanstack/virtual-file-routes': 1.161.7 + '@tanstack/router-core': 1.171.2 + '@tanstack/router-utils': 1.162.0 + '@tanstack/virtual-file-routes': 1.162.0 jiti: 2.7.0 magic-string: 0.30.21 prettier: 3.8.3 @@ -5302,7 +5566,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.35(@tanstack/react-router@1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': + '@tanstack/router-plugin@1.168.6(@tanstack/react-router@1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -5310,26 +5574,26 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.169.2 - '@tanstack/router-generator': 1.166.42 - '@tanstack/router-utils': 1.161.8 - '@tanstack/virtual-file-routes': 1.161.7 + '@tanstack/router-core': 1.171.2 + '@tanstack/router-generator': 1.167.5 + '@tanstack/router-utils': 1.162.0 + '@tanstack/virtual-file-routes': 1.162.0 chokidar: 3.6.0 unplugin: 3.0.0 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.169.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + '@tanstack/react-router': 1.170.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) transitivePeerDependencies: - supports-color - '@tanstack/router-utils@1.161.8': + '@tanstack/router-utils@1.162.0': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/types': 7.29.0 - ansis: 4.2.0 + ansis: 4.3.0 babel-dead-code-elimination: 1.0.12 diff: 8.0.4 pathe: 2.0.3 @@ -5343,7 +5607,7 @@ snapshots: '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-file-routes@1.161.7': {} + '@tanstack/virtual-file-routes@1.162.0': {} '@ts-morph/common@0.27.0': dependencies: @@ -5366,21 +5630,21 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@25.6.2': + '@types/node@25.9.1': dependencies: - undici-types: 7.19.2 + undici-types: 7.24.6 - '@types/react-dom@19.2.3(@types/react@19.2.14)': + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@types/react@19.2.14': + '@types/react@19.2.15': dependencies: csstype: 3.2.3 '@types/set-cookie-parser@2.4.10': dependencies: - '@types/node': 25.6.2 + '@types/node': 25.9.1 '@types/statuses@2.0.6': {} @@ -5390,10 +5654,10 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': + '@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))': dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + '@rolldown/pluginutils': 1.0.1 + vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0) accepts@2.0.0: dependencies: @@ -5423,7 +5687,7 @@ snapshots: dependencies: color-convert: 2.0.1 - ansis@4.2.0: {} + ansis@4.3.0: {} anymatch@3.1.3: dependencies: @@ -5888,7 +6152,7 @@ snapshots: dependencies: is-glob: 4.0.3 - goober@2.1.18(csstype@3.2.3): + goober@2.1.19(csstype@3.2.3): dependencies: csstype: 3.2.3 @@ -6122,7 +6386,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.14.0(react@19.2.6): + lucide-react@1.16.0(react@19.2.6): dependencies: react: 19.2.6 @@ -6192,9 +6456,9 @@ snapshots: ms@2.1.3: {} - msw@2.14.5(@types/node@25.6.2)(typescript@6.0.3): + msw@2.14.5(@types/node@25.9.1)(typescript@6.0.3): dependencies: - '@inquirer/confirm': 6.0.12(@types/node@25.6.2) + '@inquirer/confirm': 6.0.12(@types/node@25.9.1) '@mswjs/interceptors': 0.41.8 '@open-draft/deferred-promise': 3.0.0 '@types/statuses': 2.0.6 @@ -6297,61 +6561,89 @@ snapshots: outvariant@1.4.3: {} - oxfmt@0.48.0: + oxc-parser@0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + dependencies: + '@oxc-project/types': 0.120.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.120.0 + '@oxc-parser/binding-android-arm64': 0.120.0 + '@oxc-parser/binding-darwin-arm64': 0.120.0 + '@oxc-parser/binding-darwin-x64': 0.120.0 + '@oxc-parser/binding-freebsd-x64': 0.120.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.120.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.120.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.120.0 + '@oxc-parser/binding-linux-arm64-musl': 0.120.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.120.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.120.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-gnu': 0.120.0 + '@oxc-parser/binding-linux-x64-musl': 0.120.0 + '@oxc-parser/binding-openharmony-arm64': 0.120.0 + '@oxc-parser/binding-wasm32-wasi': 0.120.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@oxc-parser/binding-win32-arm64-msvc': 0.120.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.120.0 + '@oxc-parser/binding-win32-x64-msvc': 0.120.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + oxfmt@0.51.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.48.0 - '@oxfmt/binding-android-arm64': 0.48.0 - '@oxfmt/binding-darwin-arm64': 0.48.0 - '@oxfmt/binding-darwin-x64': 0.48.0 - '@oxfmt/binding-freebsd-x64': 0.48.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.48.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.48.0 - '@oxfmt/binding-linux-arm64-gnu': 0.48.0 - '@oxfmt/binding-linux-arm64-musl': 0.48.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.48.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.48.0 - '@oxfmt/binding-linux-riscv64-musl': 0.48.0 - '@oxfmt/binding-linux-s390x-gnu': 0.48.0 - '@oxfmt/binding-linux-x64-gnu': 0.48.0 - '@oxfmt/binding-linux-x64-musl': 0.48.0 - '@oxfmt/binding-openharmony-arm64': 0.48.0 - '@oxfmt/binding-win32-arm64-msvc': 0.48.0 - '@oxfmt/binding-win32-ia32-msvc': 0.48.0 - '@oxfmt/binding-win32-x64-msvc': 0.48.0 - - oxlint-tsgolint@0.22.1: + '@oxfmt/binding-android-arm-eabi': 0.51.0 + '@oxfmt/binding-android-arm64': 0.51.0 + '@oxfmt/binding-darwin-arm64': 0.51.0 + '@oxfmt/binding-darwin-x64': 0.51.0 + '@oxfmt/binding-freebsd-x64': 0.51.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.51.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.51.0 + '@oxfmt/binding-linux-arm64-gnu': 0.51.0 + '@oxfmt/binding-linux-arm64-musl': 0.51.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.51.0 + '@oxfmt/binding-linux-riscv64-musl': 0.51.0 + '@oxfmt/binding-linux-s390x-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-gnu': 0.51.0 + '@oxfmt/binding-linux-x64-musl': 0.51.0 + '@oxfmt/binding-openharmony-arm64': 0.51.0 + '@oxfmt/binding-win32-arm64-msvc': 0.51.0 + '@oxfmt/binding-win32-ia32-msvc': 0.51.0 + '@oxfmt/binding-win32-x64-msvc': 0.51.0 + + oxlint-tsgolint@0.23.0: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.22.1 - '@oxlint-tsgolint/darwin-x64': 0.22.1 - '@oxlint-tsgolint/linux-arm64': 0.22.1 - '@oxlint-tsgolint/linux-x64': 0.22.1 - '@oxlint-tsgolint/win32-arm64': 0.22.1 - '@oxlint-tsgolint/win32-x64': 0.22.1 - - oxlint@1.63.0(oxlint-tsgolint@0.22.1): + '@oxlint-tsgolint/darwin-arm64': 0.23.0 + '@oxlint-tsgolint/darwin-x64': 0.23.0 + '@oxlint-tsgolint/linux-arm64': 0.23.0 + '@oxlint-tsgolint/linux-x64': 0.23.0 + '@oxlint-tsgolint/win32-arm64': 0.23.0 + '@oxlint-tsgolint/win32-x64': 0.23.0 + + oxlint@1.66.0(oxlint-tsgolint@0.23.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.63.0 - '@oxlint/binding-android-arm64': 1.63.0 - '@oxlint/binding-darwin-arm64': 1.63.0 - '@oxlint/binding-darwin-x64': 1.63.0 - '@oxlint/binding-freebsd-x64': 1.63.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.63.0 - '@oxlint/binding-linux-arm-musleabihf': 1.63.0 - '@oxlint/binding-linux-arm64-gnu': 1.63.0 - '@oxlint/binding-linux-arm64-musl': 1.63.0 - '@oxlint/binding-linux-ppc64-gnu': 1.63.0 - '@oxlint/binding-linux-riscv64-gnu': 1.63.0 - '@oxlint/binding-linux-riscv64-musl': 1.63.0 - '@oxlint/binding-linux-s390x-gnu': 1.63.0 - '@oxlint/binding-linux-x64-gnu': 1.63.0 - '@oxlint/binding-linux-x64-musl': 1.63.0 - '@oxlint/binding-openharmony-arm64': 1.63.0 - '@oxlint/binding-win32-arm64-msvc': 1.63.0 - '@oxlint/binding-win32-ia32-msvc': 1.63.0 - '@oxlint/binding-win32-x64-msvc': 1.63.0 - oxlint-tsgolint: 0.22.1 + '@oxlint/binding-android-arm-eabi': 1.66.0 + '@oxlint/binding-android-arm64': 1.66.0 + '@oxlint/binding-darwin-arm64': 1.66.0 + '@oxlint/binding-darwin-x64': 1.66.0 + '@oxlint/binding-freebsd-x64': 1.66.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.66.0 + '@oxlint/binding-linux-arm-musleabihf': 1.66.0 + '@oxlint/binding-linux-arm64-gnu': 1.66.0 + '@oxlint/binding-linux-arm64-musl': 1.66.0 + '@oxlint/binding-linux-ppc64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-gnu': 1.66.0 + '@oxlint/binding-linux-riscv64-musl': 1.66.0 + '@oxlint/binding-linux-s390x-gnu': 1.66.0 + '@oxlint/binding-linux-x64-gnu': 1.66.0 + '@oxlint/binding-linux-x64-musl': 1.66.0 + '@oxlint/binding-openharmony-arm64': 1.66.0 + '@oxlint/binding-win32-arm64-msvc': 1.66.0 + '@oxlint/binding-win32-ia32-msvc': 1.66.0 + '@oxlint/binding-win32-x64-msvc': 1.66.0 + oxlint-tsgolint: 0.23.0 parent-module@1.0.1: dependencies: @@ -6399,6 +6691,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + powershell-utils@0.1.0: {} prettier@3.8.3: {} @@ -6425,68 +6723,68 @@ snapshots: queue-microtask@1.2.3: {} - radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.15)(react@19.2.6) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) range-parser@1.2.1: {} @@ -6502,32 +6800,32 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.6): + react-remove-scroll-bar@2.3.8(@types/react@19.2.15)(react@19.2.6): dependencies: react: 19.2.6 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.6): + react-remove-scroll@2.7.2(@types/react@19.2.15)(react@19.2.6): dependencies: react: 19.2.6 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.6) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.6) + react-remove-scroll-bar: 2.3.8(@types/react@19.2.15)(react@19.2.6) + react-style-singleton: 2.2.3(@types/react@19.2.15)(react@19.2.6) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.6) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.6) + use-callback-ref: 1.3.3(@types/react@19.2.15)(react@19.2.6) + use-sidecar: 1.1.3(@types/react@19.2.15)(react@19.2.6) optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.6): + react-style-singleton@2.2.3(@types/react@19.2.15)(react@19.2.6): dependencies: get-nonce: 1.0.1 react: 19.2.6 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 react@19.2.6: {} @@ -6557,6 +6855,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} restore-cursor@5.1.0: @@ -6568,26 +6868,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.18: + rolldown@1.0.1: dependencies: - '@oxc-project/types': 0.128.0 - '@rolldown/pluginutils': 1.0.0-rc.18 + '@oxc-project/types': 0.130.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 - '@rolldown/binding-darwin-x64': 1.0.0-rc.18 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + '@rolldown/binding-android-arm64': 1.0.1 + '@rolldown/binding-darwin-arm64': 1.0.1 + '@rolldown/binding-darwin-x64': 1.0.1 + '@rolldown/binding-freebsd-x64': 1.0.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 + '@rolldown/binding-linux-arm64-gnu': 1.0.1 + '@rolldown/binding-linux-arm64-musl': 1.0.1 + '@rolldown/binding-linux-ppc64-gnu': 1.0.1 + '@rolldown/binding-linux-s390x-gnu': 1.0.1 + '@rolldown/binding-linux-x64-gnu': 1.0.1 + '@rolldown/binding-linux-x64-musl': 1.0.1 + '@rolldown/binding-openharmony-arm64': 1.0.1 + '@rolldown/binding-wasm32-wasi': 1.0.1 + '@rolldown/binding-win32-arm64-msvc': 1.0.1 + '@rolldown/binding-win32-x64-msvc': 1.0.1 router@2.2.0: dependencies: @@ -6646,7 +6946,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.7.0(@types/node@25.6.2)(typescript@6.0.3): + shadcn@4.7.0(@types/node@25.9.1)(typescript@6.0.3): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.3 @@ -6667,7 +6967,7 @@ snapshots: fuzzysort: 3.1.0 https-proxy-agent: 7.0.6 kleur: 4.1.5 - msw: 2.14.5(@types/node@25.6.2)(typescript@6.0.3) + msw: 2.14.5(@types/node@25.9.1)(typescript@6.0.3) node-fetch: 3.3.2 open: 11.0.0 ora: 8.2.0 @@ -6676,7 +6976,7 @@ snapshots: prompts: 2.4.2 recast: 0.23.11 stringify-object: 5.0.0 - tailwind-merge: 3.5.0 + tailwind-merge: 3.6.0 ts-morph: 26.0.0 tsconfig-paths: 4.2.0 validate-npm-package-name: 7.0.2 @@ -6697,14 +6997,14 @@ snapshots: shell-quote@1.8.3: {} - shiki@4.0.2: + shiki@4.1.0: dependencies: - '@shikijs/core': 4.0.2 - '@shikijs/engine-javascript': 4.0.2 - '@shikijs/engine-oniguruma': 4.0.2 - '@shikijs/langs': 4.0.2 - '@shikijs/themes': 4.0.2 - '@shikijs/types': 4.0.2 + '@shikijs/core': 4.1.0 + '@shikijs/engine-javascript': 4.1.0 + '@shikijs/engine-oniguruma': 4.1.0 + '@shikijs/langs': 4.1.0 + '@shikijs/themes': 4.1.0 + '@shikijs/types': 4.1.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -6815,7 +7115,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwind-merge@3.5.0: {} + tailwind-merge@3.6.0: {} tailwindcss@4.3.0: {} @@ -6877,7 +7177,7 @@ snapshots: typescript@6.0.3: {} - undici-types@7.19.2: {} + undici-types@7.24.6: {} unicorn-magic@0.3.0: {} @@ -6931,20 +7231,20 @@ snapshots: urlpattern-polyfill@10.1.0: {} - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.6): + use-callback-ref@1.3.3(@types/react@19.2.15)(react@19.2.6): dependencies: react: 19.2.6 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.6): + use-sidecar@1.1.3(@types/react@19.2.15)(react@19.2.6): dependencies: detect-node-es: 1.1.0 react: 19.2.6 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 use-sync-external-store@1.6.0(react@19.2.6): dependencies: @@ -6975,15 +7275,15 @@ snapshots: transitivePeerDependencies: - rollup - vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0): + vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.14 - rolldown: 1.0.0-rc.18 + postcss: 8.5.15 + rolldown: 1.0.1 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.6.2 + '@types/node': 25.9.1 fsevents: 2.3.3 jiti: 2.7.0 @@ -7007,7 +7307,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.3.1: dependencies: diff --git a/frontend/src/components/badges/notification-status-badge.tsx b/frontend/src/components/badges/notification-status-badge.tsx new file mode 100644 index 0000000..2d80651 --- /dev/null +++ b/frontend/src/components/badges/notification-status-badge.tsx @@ -0,0 +1,51 @@ +import type { NotificationStatus } from "@/lib/notifications"; +import { m } from "@/lib/paraglide/messages"; +import { Badge } from "../ui/badge"; + +function getStatusLabel(status: NotificationStatus): string { + switch (status) { + case "success": + return m.successAlertTitle(); + case "error": + return m.statusError(); + case "healthy": + return m.statusHealthy(); + case "unhealthy": + return m.statusUnhealthy(); + default: + return m.unknown(); + } +} + +function getStatusDotClass(status: NotificationStatus): string { + switch (status) { + case "success": + return "bg-emerald-500"; + case "error": + return "bg-red-500"; + case "healthy": + return "bg-emerald-500"; + case "unhealthy": + return "bg-amber-500"; + default: + return "bg-zinc-400"; + } +} + +export function NotificationStatusBadge({ status }: { status: NotificationStatus }) { + const badgeVariant = + status === "success" || status === "healthy" + ? "success" + : status === "error" + ? "destructive" + : "secondary"; + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/components/dialogs/discord-notification-builder.tsx b/frontend/src/components/dialogs/discord-notification-builder.tsx new file mode 100644 index 0000000..811e7eb --- /dev/null +++ b/frontend/src/components/dialogs/discord-notification-builder.tsx @@ -0,0 +1,326 @@ +import { Field, FieldError } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { m } from "@/lib/paraglide/messages"; + +type DiscordBuilderFieldBinding = { + name: string; + state: { + value: string; + meta: { + isTouched: boolean; + isValid: boolean; + errors: unknown[]; + }; + }; + handleBlur: () => void; + handleChange: (value: string) => void; +}; + +type FieldErrorList = Array<{ message?: string } | undefined>; + +export function DiscordWebhookUrlField({ field }: { field: DiscordBuilderFieldBinding }) { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + + field.handleChange(event.target.value)} + placeholder={m.notificationWebhookUrlPlaceholder()} + /> + {isInvalid && } + + ); +} + +export function DiscordBotNameField({ field }: { field: DiscordBuilderFieldBinding }) { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + + field.handleChange(event.target.value)} + placeholder={m.notificationBotNamePlaceholder()} + /> + {isInvalid && } + + ); +} + +export function DiscordAvatarUrlField({ field }: { field: DiscordBuilderFieldBinding }) { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + + field.handleChange(event.target.value)} + placeholder={m.notificationAvatarUrlPlaceholder()} + /> + {isInvalid && } + + ); +} + +export function DiscordThreadIdField({ field }: { field: DiscordBuilderFieldBinding }) { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + + field.handleChange(event.target.value)} + placeholder={m.notificationThreadIdPlaceholder()} + /> + {isInvalid && } + + ); +} + +export type DiscordNotificationConfig = { + token: string; + webhookId: string; + threadId?: string; + username?: string; + avatarUrl?: string; +}; + +export type DiscordWebhookUrlParts = { + token: string; + webhookId: string; + threadId?: string; +}; + +export type DiscordNotificationBuilderValues = { + discordWebhookUrl: string; + discordBotName: string; + discordAvatarUrl: string; + discordThreadId: string; +}; + +const emptyDiscordNotificationBuilderValues: DiscordNotificationBuilderValues = { + discordWebhookUrl: "", + discordBotName: "", + discordAvatarUrl: "", + discordThreadId: "", +}; + +const discordHostPattern = /(^|\.)discord(?:app)?\.com$/i; + +function normalizeString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function toDiscordWebhookUrl(webhookId: string, token: string): string { + return `https://discord.com/api/webhooks/${webhookId}/${token}`; +} + +export function parseDiscordWebhookUrl(rawWebhookUrl: string): DiscordWebhookUrlParts | null { + let parsedUrl: URL; + try { + parsedUrl = new URL(rawWebhookUrl.trim()); + } catch { + return null; + } + + if (!discordHostPattern.test(parsedUrl.hostname)) { + return null; + } + + const pathParts = parsedUrl.pathname.split("/").filter(Boolean); + if ( + pathParts.length < 4 || + pathParts[0]?.toLowerCase() !== "api" || + pathParts[1]?.toLowerCase() !== "webhooks" + ) { + return null; + } + + const webhookId = pathParts[2]?.trim() ?? ""; + const token = pathParts[3]?.trim() ?? ""; + if (webhookId === "" || token === "") { + return null; + } + + const threadId = parsedUrl.searchParams.get("thread_id")?.trim() ?? ""; + + return { + token, + webhookId, + threadId: threadId === "" ? undefined : threadId, + }; +} + +function parseDiscordShoutrrrUrl(rawUrl: string): DiscordNotificationBuilderValues | null { + let parsedUrl: URL; + try { + parsedUrl = new URL(rawUrl.trim()); + } catch { + return null; + } + + if (parsedUrl.protocol !== "discord:") { + return null; + } + + const token = parsedUrl.username.trim(); + const webhookId = parsedUrl.host.trim(); + if (token === "" || webhookId === "") { + return null; + } + + return { + discordWebhookUrl: toDiscordWebhookUrl(webhookId, token), + discordBotName: parsedUrl.searchParams.get("username")?.trim() ?? "", + discordAvatarUrl: + parsedUrl.searchParams.get("avatarurl")?.trim() ?? + parsedUrl.searchParams.get("avatarUrl")?.trim() ?? + "", + discordThreadId: parsedUrl.searchParams.get("thread_id")?.trim() ?? "", + }; +} + +function parseDiscordWebhookBuilderValues( + rawWebhookUrl: string, +): DiscordNotificationBuilderValues | null { + const parsedWebhook = parseDiscordWebhookUrl(rawWebhookUrl); + if (!parsedWebhook) { + return null; + } + + return { + ...emptyDiscordNotificationBuilderValues, + discordWebhookUrl: toDiscordWebhookUrl(parsedWebhook.webhookId, parsedWebhook.token), + discordThreadId: parsedWebhook.threadId ?? "", + }; +} + +export function parseDiscordBuilderValues( + rawConfig: string | undefined, +): DiscordNotificationBuilderValues { + if (!rawConfig) { + return { ...emptyDiscordNotificationBuilderValues }; + } + + const trimmedConfig = rawConfig.trim(); + if (trimmedConfig === "") { + return { ...emptyDiscordNotificationBuilderValues }; + } + + if (trimmedConfig.startsWith("{")) { + try { + const parsedConfig = JSON.parse(trimmedConfig) as Record; + const botName = normalizeString(parsedConfig.username); + const avatarUrl = normalizeString(parsedConfig.avatarUrl); + const threadId = normalizeString(parsedConfig.threadId); + + const token = normalizeString(parsedConfig.token); + const webhookId = normalizeString(parsedConfig.webhookId); + if (token !== "" && webhookId !== "") { + return { + discordWebhookUrl: toDiscordWebhookUrl(webhookId, token), + discordBotName: botName, + discordAvatarUrl: avatarUrl, + discordThreadId: threadId, + }; + } + + const configUrls: string[] = []; + const directUrl = normalizeString(parsedConfig.url); + if (directUrl !== "") { + configUrls.push(directUrl); + } + if (Array.isArray(parsedConfig.urls)) { + for (const candidate of parsedConfig.urls) { + const normalizedCandidate = normalizeString(candidate); + if (normalizedCandidate !== "") { + configUrls.push(normalizedCandidate); + } + } + } + + for (const configUrl of configUrls) { + const parsedBuilderValues = + parseDiscordShoutrrrUrl(configUrl) ?? parseDiscordWebhookBuilderValues(configUrl); + if (parsedBuilderValues) { + return { + ...parsedBuilderValues, + discordBotName: botName === "" ? parsedBuilderValues.discordBotName : botName, + discordAvatarUrl: avatarUrl === "" ? parsedBuilderValues.discordAvatarUrl : avatarUrl, + discordThreadId: threadId === "" ? parsedBuilderValues.discordThreadId : threadId, + }; + } + } + + return { + ...emptyDiscordNotificationBuilderValues, + discordBotName: botName, + discordAvatarUrl: avatarUrl, + discordThreadId: threadId, + }; + } catch { + return { ...emptyDiscordNotificationBuilderValues }; + } + } + + return ( + parseDiscordShoutrrrUrl(trimmedConfig) ?? + parseDiscordWebhookBuilderValues(trimmedConfig) ?? { + ...emptyDiscordNotificationBuilderValues, + } + ); +} + +export function buildDiscordNotificationConfig( + values: DiscordNotificationBuilderValues, +): string | null { + const parsedWebhook = parseDiscordWebhookUrl(values.discordWebhookUrl); + if (!parsedWebhook) { + return null; + } + + const config: DiscordNotificationConfig = { + token: parsedWebhook.token, + webhookId: parsedWebhook.webhookId, + }; + + const botName = normalizeString(values.discordBotName); + if (botName !== "") { + config.username = botName; + } + + const avatarUrl = normalizeString(values.discordAvatarUrl); + if (avatarUrl !== "") { + config.avatarUrl = avatarUrl; + } + + const threadId = normalizeString(values.discordThreadId) || parsedWebhook.threadId || ""; + if (threadId !== "") { + config.threadId = threadId; + } + + return JSON.stringify(config); +} diff --git a/frontend/src/components/dialogs/upsert-notification.tsx b/frontend/src/components/dialogs/upsert-notification.tsx new file mode 100644 index 0000000..99c44fa --- /dev/null +++ b/frontend/src/components/dialogs/upsert-notification.tsx @@ -0,0 +1,377 @@ +// oxlint-disable react/no-children-prop +import { Pencil, Plus } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "@tanstack/react-form"; +import { z } from "zod"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Field, FieldError, FieldGroup } from "@/components/ui/field"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useFetch } from "@/lib/api"; +import type { ApplicationListItem } from "@/lib/applications"; +import { + createNotification, + isHttpUrl, + normalizeNotificationApplicationIds, + type Notification, + type NotificationType, + notificationTypes, + updateNotification, +} from "@/lib/notifications"; +import { m } from "@/lib/paraglide/messages"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from "@/components/ui/combobox"; +import { + buildDiscordNotificationConfig, + DiscordAvatarUrlField, + DiscordBotNameField, + DiscordThreadIdField, + DiscordWebhookUrlField, + parseDiscordBuilderValues, + parseDiscordWebhookUrl, +} from "./discord-notification-builder"; +import { Item, ItemContent, ItemDescription, ItemTitle } from "../ui/item"; + +const notificationSchema = z + .object({ + name: z + .string() + .trim() + .min(1, m.validationNotificationNameRequired()) + .max(128, m.validationNotificationNameMaxLength()), + type: z.enum(notificationTypes), + discordWebhookUrl: z.string().trim(), + discordBotName: z.string().trim(), + discordAvatarUrl: z.string().trim(), + discordThreadId: z.string().trim(), + enabled: z.boolean(), + enableByDefault: z.boolean(), + applicationIds: z.array(z.string()), + }) + .superRefine((value, ctx) => { + if (value.type === "discord") { + if (value.discordWebhookUrl === "") { + ctx.addIssue({ + code: "custom", + path: ["discordWebhookUrl"], + message: m.validationNotificationWebhookUrlRequired(), + }); + } else if (!parseDiscordWebhookUrl(value.discordWebhookUrl)) { + ctx.addIssue({ + code: "custom", + path: ["discordWebhookUrl"], + message: m.validationNotificationWebhookUrlInvalid(), + }); + } + + if (value.discordAvatarUrl !== "" && !isHttpUrl(value.discordAvatarUrl)) { + ctx.addIssue({ + code: "custom", + path: ["discordAvatarUrl"], + message: m.validationNotificationAvatarUrlInvalid(), + }); + } + + if (value.discordThreadId !== "" && !/^\d+$/.test(value.discordThreadId)) { + ctx.addIssue({ + code: "custom", + path: ["discordThreadId"], + message: m.validationNotificationThreadIdInvalid(), + }); + } + } + }); + +type NotificationFormValues = z.infer; + +function buildNotificationConfig(value: NotificationFormValues): string { + if (value.type === "discord") { + const config = buildDiscordNotificationConfig({ + discordWebhookUrl: value.discordWebhookUrl, + discordBotName: value.discordBotName, + discordAvatarUrl: value.discordAvatarUrl, + discordThreadId: value.discordThreadId, + }); + if (!config) { + throw new Error(m.validationNotificationWebhookUrlInvalid()); + } + + return config; + } + + return ""; +} + +export default function UpsertNotificationDialog({ + notification, + asDropdownItem = false, +}: { + notification: Notification | null; + asDropdownItem?: boolean; +}) { + const isEditing = notification !== null; + const [open, setOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const discordBuilderValues = parseDiscordBuilderValues(notification?.config); + + const { data: applications } = useFetch("/applications"); + + const form = useForm({ + defaultValues: { + name: notification?.name ?? "", + type: (notification?.type ?? "discord") as NotificationType, + discordWebhookUrl: discordBuilderValues.discordWebhookUrl, + discordBotName: discordBuilderValues.discordBotName, + discordAvatarUrl: discordBuilderValues.discordAvatarUrl, + discordThreadId: discordBuilderValues.discordThreadId, + enabled: notification?.enabled ?? true, + enableByDefault: notification?.enableByDefault ?? false, + applicationIds: notification?.applicationIds ?? [], + }, + validators: { + onSubmit: notificationSchema, + }, + onSubmit: async ({ value }) => { + setIsSubmitting(true); + try { + const payload = { + name: value.name, + type: value.type, + config: buildNotificationConfig(value), + enabled: value.enabled, + enableByDefault: value.enableByDefault, + applicationIds: normalizeNotificationApplicationIds(value.applicationIds), + }; + + if (notification) { + await updateNotification(notification.id, payload); + toast.success(m.notificationUpdated()); + } else { + await createNotification(payload); + toast.success(m.notificationCreated()); + } + + setOpen(false); + } catch (err) { + toast.error(err instanceof Error ? err.message : m.failedSaveNotification()); + } finally { + setIsSubmitting(false); + } + }, + }); + + return ( + + + {asDropdownItem ? ( + e.preventDefault()}> + + {m.edit()} + + ) : ( + + )} + + + + + {isEditing ? m.editNotification() : m.addNotification()} + + {isEditing ? m.editNotificationDescription() : m.addNotificationDescription()} + + + +
{ + e.preventDefault(); + await form.handleSubmit(); + }} + > + + { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( + + + field.handleChange(event.target.value)} + placeholder={m.notificationNamePlaceholder()} + autoFocus + /> + {isInvalid && } + + ); + }} + /> + + ( + + + + + )} + /> + + state.values.type}> + {(type) => + type === "discord" ? ( + <> + } + /> + + } + /> + + } + /> + + } + /> + + ) : null + } + + + ( +
+
+

{m.enabled()}

+

+ {m.notificationEnabledDescription()} +

+
+ +
+ )} + /> + + ( +
+
+

{m.enableByDefault()}

+

+ {m.enableByDefaultDescription()} +

+
+ +
+ )} + /> + + ( + + + + + + {m.noApplicationsAvailable()} + + {(item) => ( + + + + {item.name} + + {item.repositoryName} / {item.agentName} + + + + + )} + + + + + )} + /> + +
+ + +
+
+
+
+
+ ); +} diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index ca0b713..bb552fb 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -1,5 +1,6 @@ import { Link, useLocation, useNavigate } from "@tanstack/react-router"; import { + Bell, FileText, GitBranch, LayoutGrid, @@ -33,12 +34,13 @@ import { } from "@/components/ui/navigation-menu"; import HotkeysDialog from "./dialogs/hotkeys-dialog"; -type NavKey = "applications" | "agents" | "repositories" | "admin"; +type NavKey = "applications" | "agents" | "repositories" | "notifications" | "admin"; const navItems = [ { key: "applications" as NavKey, href: "/applications", icon: LayoutGrid }, { key: "agents" as NavKey, href: "/agents", icon: Server }, { key: "repositories" as NavKey, href: "/repositories", icon: GitBranch }, + { key: "notifications" as NavKey, href: "/notifications", icon: Bell }, ]; const adminNavItems = [{ key: "admin" as NavKey, href: "/admin", icon: Settings }]; @@ -51,6 +53,8 @@ function getNavLabel(key: NavKey): string { return m.navAgents(); case "repositories": return m.navRepositories(); + case "notifications": + return m.navNotifications(); case "admin": return m.navAdmin(); default: diff --git a/frontend/src/components/tables/notifications/columns.tsx b/frontend/src/components/tables/notifications/columns.tsx new file mode 100644 index 0000000..f9cad47 --- /dev/null +++ b/frontend/src/components/tables/notifications/columns.tsx @@ -0,0 +1,130 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal, Send, Trash2 } from "lucide-react"; +import { toast } from "sonner"; + +import ConfirmationDialog from "@/components/dialogs/confirm-dialog"; +import UpsertNotificationDialog from "@/components/dialogs/upsert-notification"; +import { NotificationStatusBadge } from "@/components/badges/notification-status-badge"; +import { Button } from "@/components/ui/button"; +import { DataTableColumnHeader } from "@/components/tables/data-table-column-header"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { m } from "@/lib/paraglide/messages"; +import { deleteNotification, type Notification, testNotification } from "@/lib/notifications"; + +function formatDate(value: string): string { + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return value; + } + + return parsed.toLocaleString(); +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => , + }, + { + id: "status", + accessorFn: (row) => row.status, + header: ({ column }) => , + cell: ({ row }) => , + }, + { + id: "type", + accessorFn: (row) => row.type, + header: ({ column }) => , + cell: ({ row }) => row.original.type, + }, + { + id: "enabled", + accessorFn: (row) => String(row.enabled), + header: ({ column }) => , + cell: ({ row }) => (row.original.enabled ? m.enabled() : m.disabled()), + }, + { + id: "apps", + accessorFn: (row) => row.applicationIds.length, + header: ({ column }) => , + cell: ({ row }) => row.original.applicationIds.length, + }, + { + id: "updatedAt", + accessorFn: (row) => row.updatedAt, + header: ({ column }) => , + cell: ({ row }) => formatDate(row.original.updatedAt), + }, + { + id: "createdAt", + accessorFn: (row) => row.createdAt, + header: ({ column }) => , + cell: ({ row }) => formatDate(row.original.createdAt), + }, + { + id: "actions", + cell: ({ row }) => { + async function handleDelete(item: Notification) { + try { + await deleteNotification(item.id); + const identifier = item.name.trim() || item.id; + toast.success(m.notificationDeleted({ name: identifier })); + } catch (err) { + toast.error(err instanceof Error ? err.message : m.failedDeleteNotification()); + } + } + + async function handleTest(item: Notification) { + try { + await testNotification(item.id); + toast.success(m.testNotificationSent()); + } catch (err) { + toast.error(err instanceof Error ? err.message : m.failedSendTestNotification()); + } + } + + return ( +
+ + + + + + {m.actions()} + handleTest(row.original)}> + + {m.sendTest()} + + + + { + void handleDelete(row.original); + }} + title={m.deleteNotificationTitle()} + description={m.deleteNotificationDescription({ name: row.original.name })} + triggerText={ + <> + + {m.delete()} + + } + asDropdownItem + /> + + +
+ ); + }, + }, +]; diff --git a/frontend/src/components/tables/notifications/data-table.tsx b/frontend/src/components/tables/notifications/data-table.tsx new file mode 100644 index 0000000..be3324c --- /dev/null +++ b/frontend/src/components/tables/notifications/data-table.tsx @@ -0,0 +1,98 @@ +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, +} from "@tanstack/react-table"; +import { useState } from "react"; + +import { DataTableViewOptions } from "@/components/tables/data-table-view-options"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { m } from "@/lib/paraglide/messages"; + +interface NotificationsDataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function NotificationsDataTable({ + columns, + data, +}: NotificationsDataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({ + createdAt: false, + }); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnVisibility, + }, + }); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {m.noResults()} + + + )} + +
+
+
+
+ {m.totalNotificationsCount({ count: data.length })} +
+ +
+
+ ); +} diff --git a/frontend/src/components/ui/combobox.tsx b/frontend/src/components/ui/combobox.tsx new file mode 100644 index 0000000..7cd0d2e --- /dev/null +++ b/frontend/src/components/ui/combobox.tsx @@ -0,0 +1,275 @@ +"use client"; + +import * as React from "react"; +import { Combobox as ComboboxPrimitive } from "@base-ui/react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@/components/ui/input-group"; +import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"; + +const Combobox = ComboboxPrimitive.Root; + +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { + return ; +} + +function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) { + return ( + + {children} + + + ); +} + +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { + return ( + } + className={cn(className)} + {...props} + > + + + ); +} + +function ComboboxInput({ + className, + children, + disabled = false, + showTrigger = true, + showClear = false, + ...props +}: ComboboxPrimitive.Input.Props & { + showTrigger?: boolean; + showClear?: boolean; +}) { + return ( + + } {...props} /> + + {showTrigger && ( + + + + )} + {showClear && } + + {children} + + ); +} + +function ComboboxContent({ + className, + side = "bottom", + sideOffset = 6, + align = "start", + alignOffset = 0, + anchor, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick< + ComboboxPrimitive.Positioner.Props, + "side" | "align" | "sideOffset" | "alignOffset" | "anchor" + >) { + return ( + + + + + + ); +} + +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { + return ( + + ); +} + +function ComboboxItem({ className, children, ...props }: ComboboxPrimitive.Item.Props) { + return ( + + {children} + + } + > + + + + ); +} + +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { + return ( + + ); +} + +function ComboboxLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) { + return ( + + ); +} + +function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) { + return ; +} + +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { + return ( + + ); +} + +function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) { + return ( + + ); +} + +function ComboboxChips({ + className, + ...props +}: React.ComponentPropsWithRef & ComboboxPrimitive.Chips.Props) { + return ( + + ); +} + +function ComboboxChip({ + className, + children, + showRemove = true, + ...props +}: ComboboxPrimitive.Chip.Props & { + showRemove?: boolean; +}) { + return ( + + {children} + {showRemove && ( + } + className="-ml-1 opacity-50 hover:opacity-100" + data-slot="combobox-chip-remove" + > + + + )} + + ); +} + +function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) { + return ( + + ); +} + +function useComboboxAnchor() { + return React.useRef(null); +} + +export { + Combobox, + ComboboxInput, + ComboboxContent, + ComboboxList, + ComboboxItem, + ComboboxGroup, + ComboboxLabel, + ComboboxCollection, + ComboboxEmpty, + ComboboxSeparator, + ComboboxChips, + ComboboxChip, + ComboboxChipsInput, + ComboboxTrigger, + ComboboxValue, + useComboboxAnchor, +}; diff --git a/frontend/src/components/ui/input-group.tsx b/frontend/src/components/ui/input-group.tsx new file mode 100644 index 0000000..b7a7830 --- /dev/null +++ b/frontend/src/components/ui/input-group.tsx @@ -0,0 +1,142 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; + +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", + "inline-end": "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", + "block-start": + "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", + "block-end": + "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", + sm: "", + "icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, +}); + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & + VariantProps) { + return ( +