diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f4e95777..b32791ca 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,10 +28,11 @@ jobs: - id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/cooksleep/gpt_image_playground + images: ghcr.io/${{ github.repository }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + type=sha,format=short type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} - uses: docker/build-push-action@v6 diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 0929096e..a7a08894 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,12 +1,19 @@ # ---- Build stage ---- -FROM node:20-alpine AS build +# Alpine + npm ci 在 Rollup optional native package 上经常丢依赖,改用 glibc 基础镜像规避。 +FROM node:20-bookworm-slim AS build WORKDIR /app +ARG TARGETARCH ENV VITE_DEFAULT_API_URL=__VITE_DEFAULT_API_URL_PLACEHOLDER__ COPY package.json package-lock.json ./ RUN npm ci +RUN case "$TARGETARCH" in \ + "amd64") npm install --no-save @rollup/rollup-linux-x64-gnu ;; \ + "arm64") npm install --no-save @rollup/rollup-linux-arm64-gnu ;; \ + *) echo "Unsupported TARGETARCH: $TARGETARCH" && exit 1 ;; \ + esac COPY . . RUN npm run build diff --git a/package-lock.json b/package-lock.json index cf8b243c..e820dbb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "gpt-image-playground", "version": "0.2.15", "dependencies": { + "fabric": "^7.3.1", "fflate": "^0.8.2", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -38,6 +39,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "optional": true + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -320,129 +342,125 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, - "license": "MIT", + "license": "MIT-0", "optional": true, - "os": [ - "android" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], "engines": { "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { + "node_modules/@esbuild/win32-x64": { "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -450,734 +468,106 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "win32" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" + "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" - ], + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.60.2", @@ -1438,6 +828,16 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1513,6 +913,27 @@ "postcss": "^8.1.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/baseline-browser-mapping": { "version": "2.10.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", @@ -1539,6 +960,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1586,6 +1019,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1617,6 +1075,21 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1665,6 +1138,13 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1695,6 +1175,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1702,11 +1196,25 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1720,6 +1228,49 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT", + "optional": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1741,6 +1292,29 @@ "dev": true, "license": "ISC" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -1820,6 +1394,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1830,6 +1414,19 @@ "node": ">=12.0.0" } }, + "node_modules/fabric": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-7.3.1.tgz", + "integrity": "sha512-RoLAQzUX+/3RNMYKliuN0P2HXdSDEGzyjS7FnmEbo3nhb8LFh59T+l3f6ApIu5LT4YB49YfMNrEajeIbutmD7Q==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "canvas": "^3.2.0", + "jsdom": "^26.1.0" + } + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1903,20 +1500,12 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "optional": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -1938,6 +1527,13 @@ "node": ">=6.9.0" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1964,6 +1560,95 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2026,6 +1711,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT", + "optional": true + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -2043,6 +1735,46 @@ "dev": true, "license": "MIT" }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2133,11 +1865,41 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/mz": { @@ -2165,12 +1927,52 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -2188,6 +1990,13 @@ "node": ">=0.10.0" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "license": "MIT", + "optional": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2219,6 +2028,29 @@ ], "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2436,6 +2268,55 @@ "dev": true, "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2457,6 +2338,22 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", @@ -2498,6 +2395,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2589,6 +2501,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT", + "optional": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2613,6 +2532,47 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "optional": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2636,6 +2596,53 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2660,6 +2667,26 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -2696,6 +2723,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT", + "optional": true + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -2734,6 +2768,36 @@ "node": ">=14.0.0" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2832,6 +2896,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT", + "optional": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2845,6 +2929,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2852,6 +2962,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2901,7 +3024,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/vite": { @@ -3113,6 +3236,67 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3130,6 +3314,52 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT", + "optional": true + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index e2d586b0..974db824 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test:watch": "vitest" }, "dependencies": { + "fabric": "^7.3.1", "fflate": "^0.8.2", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/App.tsx b/src/App.tsx index 0ce19a56..7e9c3e83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,16 +14,14 @@ import ConfirmDialog from './components/ConfirmDialog' import Toast from './components/Toast' import MaskEditorModal from './components/MaskEditorModal' import ImageContextMenu from './components/ImageContextMenu' +import { isApplyingWebDavSnapshot, scheduleWebDavSync, setupWebDavAutoSync, syncWebDavOnLaunch } from './lib/webdavSync' export default function App() { const setSettings = useStore((s) => s.setSettings) useEffect(() => { const searchParams = new URLSearchParams(window.location.search) - const nextSettings: { baseUrl?: string; apiKey?: string; codexCli?: boolean; apiMode?: ApiMode } = { - codexCli: false, - apiMode: 'images', - } + const nextSettings: { baseUrl?: string; apiKey?: string; codexCli?: boolean; apiMode?: ApiMode } = {} const apiUrlParam = searchParams.get('apiUrl') if (apiUrlParam !== null) { @@ -45,9 +43,8 @@ export default function App() { nextSettings.apiMode = apiModeParam } - setSettings(nextSettings) - if (searchParams.has('apiUrl') || searchParams.has('apiKey') || searchParams.has('codexCli') || searchParams.has('apiMode')) { + setSettings(nextSettings) searchParams.delete('apiUrl') searchParams.delete('apiKey') searchParams.delete('codexCli') @@ -58,7 +55,50 @@ export default function App() { window.history.replaceState(null, '', nextUrl) } - initStore() + let disposed = false + let unsubscribeStore: (() => void) | null = null + let cleanupAutoSync: (() => void) | null = null + + void (async () => { + await initStore() + await syncWebDavOnLaunch() + + if (disposed) return + + cleanupAutoSync = setupWebDavAutoSync() + unsubscribeStore = useStore.subscribe((state, prevState) => { + if (isApplyingWebDavSnapshot()) return + if (state.settings.storageMode !== 'webdav') return + if (!state.settings.webdav.url.trim()) return + + const tasksChanged = state.tasks !== prevState.tasks + const settingsChanged = + state.settings !== prevState.settings && + ( + state.settings.baseUrl !== prevState.settings.baseUrl || + state.settings.apiKey !== prevState.settings.apiKey || + state.settings.model !== prevState.settings.model || + state.settings.timeout !== prevState.settings.timeout || + state.settings.apiMode !== prevState.settings.apiMode || + state.settings.codexCli !== prevState.settings.codexCli || + state.settings.storageMode !== prevState.settings.storageMode || + state.settings.webdav.url !== prevState.settings.webdav.url || + state.settings.webdav.username !== prevState.settings.webdav.username || + state.settings.webdav.password !== prevState.settings.webdav.password || + state.settings.webdav.syncOnStartup !== prevState.settings.webdav.syncOnStartup + ) + + if (tasksChanged || settingsChanged) { + scheduleWebDavSync() + } + }) + })() + + return () => { + disposed = true + unsubscribeStore?.() + cleanupAutoSync?.() + } }, [setSettings]) useEffect(() => { diff --git a/src/components/DetailModal.tsx b/src/components/DetailModal.tsx index 4b1fa5bd..44cf99e2 100644 --- a/src/components/DetailModal.tsx +++ b/src/components/DetailModal.tsx @@ -11,6 +11,7 @@ export default function DetailModal() { const detailTaskId = useStore((s) => s.detailTaskId) const setDetailTaskId = useStore((s) => s.setDetailTaskId) const setLightboxImageId = useStore((s) => s.setLightboxImageId) + const setLightboxStartEditor = useStore((s) => s.setLightboxStartEditor) const setMaskEditorImageId = useStore((s) => s.setMaskEditorImageId) const setConfirmDialog = useStore((s) => s.setConfirmDialog) const showToast = useStore((s) => s.showToast) @@ -193,6 +194,13 @@ export default function DetailModal() { setDetailTaskId(null) } + const handleQuickEditOutput = () => { + if (!currentOutputImageId) return + setDetailTaskId(null) + setLightboxStartEditor(true) + setLightboxImageId(currentOutputImageId, task.outputImages) + } + const handleMaskEditCurrentOutput = () => { const imgId = task.outputImages?.[imageIndex] if (!imgId) return @@ -350,6 +358,26 @@ export default function DetailModal() { )} +
+ + +
)} {task.status === 'running' && ( diff --git a/src/components/ImageContextMenu.tsx b/src/components/ImageContextMenu.tsx index bd50ebf1..8469251a 100644 --- a/src/components/ImageContextMenu.tsx +++ b/src/components/ImageContextMenu.tsx @@ -21,6 +21,9 @@ export default function ImageContextMenu() { // 忽略没有 src 或空的 img if (!imgTarget.src) return + // 当前页面不是安全上下文时,放行浏览器原生右键菜单,避免图片复制失效。 + if (!window.isSecureContext) return + e.preventDefault() setMenuInfo({ src: imgTarget.src, diff --git a/src/components/InputBar.tsx b/src/components/InputBar.tsx index 786146ad..d450d041 100644 --- a/src/components/InputBar.tsx +++ b/src/components/InputBar.tsx @@ -23,11 +23,21 @@ function ButtonTooltip({ visible, text }: { visible: boolean; text: string }) { const API_MAX_IMAGES = 16 function useIsMobile() { - const [isMobile, setIsMobile] = useState(window.innerWidth < 640) + const getIsMobile = () => { + const ua = navigator.userAgent || '' + const platform = navigator.platform || '' + const isIpadOS = platform === 'MacIntel' && navigator.maxTouchPoints > 1 + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i.test(ua) || isIpadOS + } + const [isMobile, setIsMobile] = useState(getIsMobile) useEffect(() => { - const onResize = () => setIsMobile(window.innerWidth < 640) + const onResize = () => setIsMobile(getIsMobile()) window.addEventListener('resize', onResize) - return () => window.removeEventListener('resize', onResize) + window.addEventListener('orientationchange', onResize) + return () => { + window.removeEventListener('resize', onResize) + window.removeEventListener('orientationchange', onResize) + } }, []) return isMobile } @@ -43,6 +53,7 @@ export default function InputBar() { const settings = useStore((s) => s.settings) const setShowSettings = useStore((s) => s.setShowSettings) const setLightboxImageId = useStore((s) => s.setLightboxImageId) + const setLightboxStartEditor = useStore((s) => s.setLightboxStartEditor) const setConfirmDialog = useStore((s) => s.setConfirmDialog) const selectedTaskIds = useStore((s) => s.selectedTaskIds) const setSelectedTaskIds = useStore((s) => s.setSelectedTaskIds) @@ -121,6 +132,7 @@ export default function InputBar() { const [moderationHintVisible, setModerationHintVisible] = useState(false) const [qualityHintVisible, setQualityHintVisible] = useState(false) const [mobileCollapsed, setMobileCollapsed] = useState(false) + const [selectedInputImageId, setSelectedInputImageId] = useState(null) const [showSizePicker, setShowSizePicker] = useState(false) const [maskPreviewUrl, setMaskPreviewUrl] = useState('') const handleRef = useRef(null) @@ -144,6 +156,26 @@ export default function InputBar() { ? inputImages.filter((img) => img.id !== maskTargetImage.id) : inputImages + useEffect(() => { + if (!selectedInputImageId) return + if (!inputImages.some((image) => image.id === selectedInputImageId)) { + setSelectedInputImageId(null) + } + }, [inputImages, selectedInputImageId]) + + useEffect(() => { + if (!isMobile || !selectedInputImageId) return + + const handleOutsidePointer = (event: PointerEvent) => { + const target = event.target + if (target instanceof Node && imagesRef.current?.contains(target)) return + setSelectedInputImageId(null) + } + + document.addEventListener('pointerdown', handleOutsidePointer, true) + return () => document.removeEventListener('pointerdown', handleOutsidePointer, true) + }, [isMobile, selectedInputImageId]) + useEffect(() => { setOutputCompressionInput( params.output_compression == null ? '' : String(params.output_compression), @@ -344,6 +376,7 @@ export default function InputBar() { // 粘贴图片 useEffect(() => { const handlePaste = (e: ClipboardEvent) => { + if (document.body.dataset.referenceEditorActive === '1') return const items = e.clipboardData?.items if (!items) return const imageFiles: File[] = [] @@ -487,19 +520,51 @@ export default function InputBar() { const selectClass = 'px-3 py-1.5 rounded-xl border border-gray-200/60 dark:border-white/[0.08] bg-white/50 dark:bg-white/[0.03] hover:bg-white dark:hover:bg-white/[0.06] text-xs transition-all duration-200 shadow-sm' + const handleInputImageClick = (imgId: string) => { + if (isMobile) { + if (selectedInputImageId === imgId) { + setLightboxStartEditor(true) + setLightboxImageId(imgId, inputImages.map((image) => image.id)) + } else { + setSelectedInputImageId(imgId) + } + return + } + setLightboxImageId(imgId, inputImages.map((image) => image.id)) + } + + const handleRemoveInputImage = (idx: number, imgId: string) => { + removeInputImage(idx) + if (selectedInputImageId === imgId) { + setSelectedInputImageId(null) + } + } const renderImageThumb = (img: (typeof inputImages)[number]) => { const originalIndex = inputImages.findIndex((i) => i.id === img.id) const isMaskTarget = maskDraft?.targetImageId === img.id const canEdit = !maskTargetImage || isMaskTarget const displaySrc = isMaskTarget && maskPreviewUrl ? maskPreviewUrl : img.dataUrl + const selected = selectedInputImageId === img.id + const deleteButtonClass = isMobile + ? selected + ? 'h-7 w-7 opacity-100' + : 'h-7 w-7 pointer-events-none opacity-0' + : 'h-[22px] w-[22px] opacity-0 group-hover:opacity-100' return (
-
setLightboxImageId(img.id, inputImages.map((i) => i.id))} + onClick={() => handleInputImageClick(img.id)} + aria-pressed={selected} + aria-label={selected ? '取消选择参考图' : '选择参考图'} > )} {canEdit && ( - )} -
- +
) } diff --git a/src/components/Lightbox.tsx b/src/components/Lightbox.tsx index 1f3e35fc..e34fdcb6 100644 --- a/src/components/Lightbox.tsx +++ b/src/components/Lightbox.tsx @@ -1,8 +1,10 @@ -import { useEffect, useState, useRef, useCallback } from 'react' +import { Suspense, lazy, useEffect, useState, useRef, useCallback } from 'react' import { useStore, getCachedImage, ensureImageCached } from '../store' import { useCloseOnEscape } from '../hooks/useCloseOnEscape' import { createMaskPreviewDataUrl } from '../lib/canvasImage' +const ReferenceImageEditorModal = lazy(() => import('./ReferenceImageEditorModal')) + const MIN_SCALE = 1 const MAX_SCALE = 10 @@ -10,14 +12,22 @@ function clamp(v: number, min: number, max: number) { return Math.max(min, Math.min(max, v)) } +function isLightboxControl(target: EventTarget | null) { + return target instanceof Element && Boolean(target.closest('[data-lightbox-control]')) +} + export default function Lightbox() { const lightboxImageId = useStore((s) => s.lightboxImageId) const lightboxImageList = useStore((s) => s.lightboxImageList) + const lightboxStartEditor = useStore((s) => s.lightboxStartEditor) const setLightboxImageId = useStore((s) => s.setLightboxImageId) + const setLightboxStartEditor = useStore((s) => s.setLightboxStartEditor) + const inputImages = useStore((s) => s.inputImages) + const [src, setSrc] = useState('') + const [showEditor, setShowEditor] = useState(false) const maskDraft = useStore((s) => s.maskDraft) const tasks = useStore((s) => s.tasks) - const [src, setSrc] = useState('') const [maskImageSrc, setMaskImageSrc] = useState('') const [maskPreviewSrc, setMaskPreviewSrc] = useState('') @@ -28,6 +38,8 @@ export default function Lightbox() { useEffect(() => { if (!lightboxImageId) { setSrc('') + setShowEditor(false) + setLightboxStartEditor(false) return } const cached = getCachedImage(lightboxImageId) @@ -38,7 +50,15 @@ export default function Lightbox() { if (url) setSrc(url) }) } - }, [lightboxImageId]) + }, [lightboxImageId, setLightboxStartEditor]) + + const isReferenceImage = Boolean(lightboxImageId && inputImages.some((image) => image.id === lightboxImageId)) + + useEffect(() => { + if (!lightboxImageId || !src || !lightboxStartEditor) return + setShowEditor(true) + setLightboxStartEditor(false) + }, [lightboxImageId, lightboxStartEditor, setLightboxStartEditor, src]) // 遮罩图加载 useEffect(() => { @@ -116,16 +136,36 @@ export default function Lightbox() { if (!lightboxImageId || !src) return null return ( - + <> + setShowEditor(true)} + /> + {showEditor && lightboxImageId && src ? ( + + 正在加载编辑器... + + } + > + setShowEditor(false)} + /> + + ) : null} + ) } @@ -138,10 +178,12 @@ interface LightboxInnerProps { total: number onPrev: () => void onNext: () => void + isReferenceImage: boolean + onEdit: () => void } /** 内部组件:保证挂载时 DOM 已经存在,所有 ref / effect 都可靠 */ -function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, total, onPrev, onNext }: LightboxInnerProps) { +function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, total, onPrev, onNext, isReferenceImage, onEdit }: LightboxInnerProps) { const containerRef = useRef(null) // 用 ref 追踪最新变换,避免闭包过期 @@ -295,6 +337,7 @@ function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, to // ====== 单击关闭(仅未缩放且非拖拽) ====== const onClick = useCallback((e: React.MouseEvent) => { + if (isLightboxControl(e.target)) return if (suppressNextClickRef.current) { suppressNextClickRef.current = false e.stopPropagation() @@ -324,6 +367,12 @@ function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, to if (!el) return const onTouchStart = (e: TouchEvent) => { + if (isLightboxControl(e.target)) { + tapRef.current = { time: 0, x: 0, y: 0 } + touchStartedOnImageRef.current = false + return + } + if (e.touches.length === 2) { e.preventDefault() hadMultiTouchRef.current = true @@ -398,6 +447,11 @@ function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, to } const onTouchEnd = (e: TouchEvent) => { + if (isLightboxControl(e.target)) { + tapRef.current = { time: 0, x: 0, y: 0 } + return + } + if (e.touches.length < 2) pinchRef.current.active = false if (e.touches.length === 0) { dragRef.current.active = false @@ -450,6 +504,19 @@ function LightboxInner({ src, maskPreviewSrc, onClose, showNav, currentIndex, to onDoubleClick={onDoubleClick} >
+
+
+
@@ -1025,6 +1064,28 @@ export default function MaskEditorModal() { , document.body, )} + {referenceEditorSession + ? createPortal( +
+ + 正在加载高级编辑器... +
+ } + > + setReferenceEditorSession(null)} + /> + + , + document.body, + ) + : null} ) } diff --git a/src/components/ReferenceImageEditorModal.tsx b/src/components/ReferenceImageEditorModal.tsx new file mode 100644 index 00000000..e752f5cb --- /dev/null +++ b/src/components/ReferenceImageEditorModal.tsx @@ -0,0 +1,1343 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Canvas, Ellipse, FabricImage, IText, PencilBrush, Rect, Triangle, type FabricObject } from 'fabric' +import { addInputImageWithDataUrl, replaceInputImageWithDataUrl, useStore } from '../store' +import { useCloseOnEscape } from '../hooks/useCloseOnEscape' + +interface ReferenceImageEditorModalProps { + imageId: string + src: string + saveMode: 'replace-input' | 'append-input' + onClose: () => void + onSaved?: (nextImageId: string, nextDataUrl: string) => void +} + +type ToolMode = 'select' | 'mask-brush' +type MaskShapeType = 'rect' | 'ellipse' | 'triangle' + +interface TextStyleState { + text: string + fill: string + fontSize: number + fontWeight: 'normal' | 'bold' + fontStyle: 'normal' | 'italic' +} + +type BaseImageData = { + editorKind: 'base-image' + flipX: boolean + flipY: boolean +} + +const DEFAULT_TEXT_STYLE: TextStyleState = { + text: '输入文字', + fill: '#ffffff', + fontSize: 48, + fontWeight: 'bold', + fontStyle: 'normal', +} + +const DEFAULT_MASK_HUE = 0 + +function clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)) +} + +function buildMaskColor(hue: number, alpha: number) { + return `hsla(${Math.round(hue)}, 85%, 48%, ${clamp(alpha, 0.05, 1)})` +} + +function getEditorKind(object: FabricObject | null): string | undefined { + if (!object || typeof object !== 'object') return undefined + return (object as FabricObject & { data?: { editorKind?: string } }).data?.editorKind +} + +function getBaseImageData(object: FabricObject | null): BaseImageData | undefined { + if (getEditorKind(object) !== 'base-image') return undefined + return (object as FabricObject & { data?: BaseImageData }).data +} + +function getMaskShapeTypeFromObject(object: FabricObject | null): MaskShapeType | undefined { + if (!object || typeof object !== 'object') return undefined + return (object as FabricObject & { data?: { shapeType?: MaskShapeType } }).data?.shapeType +} + +function useIsMobileDevice() { + const getIsMobileDevice = () => { + const ua = navigator.userAgent || '' + const platform = navigator.platform || '' + const isIpadOS = platform === 'MacIntel' && navigator.maxTouchPoints > 1 + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|Tablet/i.test(ua) || isIpadOS + } + const [isMobileDevice, setIsMobileDevice] = useState(getIsMobileDevice) + useEffect(() => { + const update = () => setIsMobileDevice(getIsMobileDevice()) + window.addEventListener('resize', update) + window.addEventListener('orientationchange', update) + return () => { + window.removeEventListener('resize', update) + window.removeEventListener('orientationchange', update) + } + }, []) + return isMobileDevice +} + +export default function ReferenceImageEditorModal({ imageId, src, saveMode, onClose, onSaved }: ReferenceImageEditorModalProps) { + const setLightboxImageId = useStore((s) => s.setLightboxImageId) + const inputImages = useStore((s) => s.inputImages) + const lightboxImageList = useStore((s) => s.lightboxImageList) + const showToast = useStore((s) => s.showToast) + const backgroundCanvasRef = useRef(null) + const canvasElementRef = useRef(null) + const canvasViewportRef = useRef(null) + const fileInputRef = useRef(null) + const canvasRef = useRef(null) + const sourceImageRef = useRef(null) + const historyRef = useRef([]) + const historyIndexRef = useRef(-1) + const suppressHistoryRef = useRef(false) + const displaySizeRef = useRef({ width: 0, height: 0 }) + const sourceSizeRef = useRef({ width: 0, height: 0 }) + const imageBoundsRef = useRef({ left: 0, top: 0, width: 0, height: 0 }) + const exportMultiplierRef = useRef(1) + const baseFlipRef = useRef({ x: false, y: false }) + const zoomRef = useRef(1) + const panRef = useRef({ x: 0, y: 0 }) + const panningRef = useRef(false) + const touchGestureRef = useRef({ + active: false, + distance: 0, + zoom: 1, + panX: 0, + panY: 0, + centerX: 0, + centerY: 0, + }) + const lastPointerRef = useRef({ x: 0, y: 0 }) + const spacePressedRef = useRef(false) + const toolModeRef = useRef('select') + const [ready, setReady] = useState(false) + const [toolMode, setToolMode] = useState('select') + const [maskOpacity, setMaskOpacity] = useState(0.55) + const [maskWidth, setMaskWidth] = useState(34) + const [maskHue, setMaskHue] = useState(DEFAULT_MASK_HUE) + const [maskShapeType, setMaskShapeType] = useState('rect') + const [activeObject, setActiveObject] = useState(null) + const [textStyle, setTextStyle] = useState(DEFAULT_TEXT_STYLE) + const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false }) + const currentImageList = useMemo(() => lightboxImageList.length ? lightboxImageList : inputImages.map((item) => item.id), [inputImages, lightboxImageList]) + const isOpen = true + const isMobileDevice = useIsMobileDevice() + + useCloseOnEscape(isOpen, onClose) + + function redrawBackgroundNow() { + const canvas = backgroundCanvasRef.current + const image = sourceImageRef.current + if (!canvas || !image) return + + const context = canvas.getContext('2d') + if (!context) return + + context.setTransform(1, 0, 0, 1, 0, 0) + context.clearRect(0, 0, canvas.width, canvas.height) + context.fillStyle = '#11161d' + context.fillRect(0, 0, canvas.width, canvas.height) + context.imageSmoothingEnabled = true + context.imageSmoothingQuality = 'high' + context.setTransform(zoomRef.current, 0, 0, zoomRef.current, panRef.current.x, panRef.current.y) + const bounds = imageBoundsRef.current + if (baseFlipRef.current.x || baseFlipRef.current.y) { + context.translate(bounds.left + bounds.width / 2, bounds.top + bounds.height / 2) + context.scale(baseFlipRef.current.x ? -1 : 1, baseFlipRef.current.y ? -1 : 1) + context.drawImage(image, -bounds.width / 2, -bounds.height / 2, bounds.width, bounds.height) + return + } + context.drawImage(image, bounds.left, bounds.top, bounds.width, bounds.height) + } + + useEffect(() => { + toolModeRef.current = toolMode + }, [toolMode]) + + useEffect(() => { + const previousOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + document.body.dataset.referenceEditorActive = '1' + return () => { + document.body.style.overflow = previousOverflow + delete document.body.dataset.referenceEditorActive + } + }, []) + + const updateHistoryFlags = useCallback(() => { + setHistoryState({ + canUndo: historyIndexRef.current > 0, + canRedo: historyIndexRef.current < historyRef.current.length - 1, + }) + }, []) + + const syncTextStyleFromObject = useCallback((object: FabricObject | null) => { + if (!(object instanceof IText)) { + setTextStyle((prev) => ({ ...prev })) + return + } + + setTextStyle({ + text: object.text ?? DEFAULT_TEXT_STYLE.text, + fill: typeof object.fill === 'string' ? object.fill : DEFAULT_TEXT_STYLE.fill, + fontSize: typeof object.fontSize === 'number' ? object.fontSize : DEFAULT_TEXT_STYLE.fontSize, + fontWeight: object.fontWeight === 'bold' ? 'bold' : 'normal', + fontStyle: object.fontStyle === 'italic' ? 'italic' : 'normal', + }) + }, []) + + const pushHistory = useCallback(() => { + const canvas = canvasRef.current + if (!canvas || suppressHistoryRef.current) return + + const serialized = JSON.stringify( + (canvas as Canvas & { toJSON: (propertiesToInclude?: string[]) => unknown }).toJSON(['data']), + ) + if (historyRef.current[historyIndexRef.current] === serialized) { + updateHistoryFlags() + return + } + + historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1) + historyRef.current.push(serialized) + historyIndexRef.current = historyRef.current.length - 1 + updateHistoryFlags() + }, [updateHistoryFlags]) + + const restoreHistory = useCallback(async (nextIndex: number) => { + const canvas = canvasRef.current + const snapshot = historyRef.current[nextIndex] + if (!canvas || !snapshot) return + + suppressHistoryRef.current = true + await canvas.loadFromJSON(JSON.parse(snapshot)) + canvas.renderAll() + suppressHistoryRef.current = false + historyIndexRef.current = nextIndex + const currentActive = canvas.getActiveObject() ?? null + const baseProxy = canvas.getObjects().find((object) => getEditorKind(object) === 'base-image') ?? null + const baseData = getBaseImageData(baseProxy) + baseFlipRef.current = { x: Boolean(baseData?.flipX), y: Boolean(baseData?.flipY) } + redrawBackgroundNow() + setActiveObject(currentActive) + syncTextStyleFromObject(currentActive) + updateHistoryFlags() + }, [syncTextStyleFromObject, updateHistoryFlags]) + + const refreshBrush = useCallback(() => { + const canvas = canvasRef.current + if (!canvas) return + + const alpha = clamp(maskOpacity, 0.05, 1) + const width = clamp(maskWidth, 4, 160) + const brush = new PencilBrush(canvas) + brush.color = buildMaskColor(maskHue, alpha) + brush.width = width + canvas.freeDrawingBrush = brush + canvas.isDrawingMode = toolMode === 'mask-brush' + canvas.defaultCursor = toolMode === 'mask-brush' ? 'crosshair' : 'default' + }, [maskHue, maskOpacity, maskWidth, toolMode]) + + const handleToolModeChange = useCallback((nextToolMode: ToolMode) => { + const canvas = canvasRef.current + if (canvas) { + canvas.discardActiveObject() + canvas.requestRenderAll() + } + setActiveObject(null) + setToolMode(nextToolMode) + }, []) + + const redrawBackground = useCallback(() => { + redrawBackgroundNow() + }, []) + + const resizeStageToViewport = useCallback((options?: { preserveViewport?: boolean; transformObjects?: boolean }) => { + const viewport = canvasViewportRef.current + const backgroundElement = backgroundCanvasRef.current + const canvas = canvasRef.current + const image = sourceImageRef.current + if (!viewport || !backgroundElement || !canvas || !image) return + + const stageRect = viewport.getBoundingClientRect() + const stageWidth = Math.max(320, Math.floor(stageRect.width)) + const stageHeight = Math.max(320, Math.floor(stageRect.height)) + const prevDisplaySize = displaySizeRef.current + const prevBounds = imageBoundsRef.current + + if ( + prevDisplaySize.width === stageWidth && + prevDisplaySize.height === stageHeight && + backgroundElement.width === stageWidth && + backgroundElement.height === stageHeight + ) { + return + } + + const imageScale = Math.min( + stageWidth / Math.max(1, image.naturalWidth), + stageHeight / Math.max(1, image.naturalHeight), + 1, + ) + const imageDisplayWidth = Math.max(1, Math.round(image.naturalWidth * imageScale)) + const imageDisplayHeight = Math.max(1, Math.round(image.naturalHeight * imageScale)) + const imageLeft = Math.max(0, Math.round((stageWidth - imageDisplayWidth) / 2)) + const imageTop = Math.max(0, Math.round((stageHeight - imageDisplayHeight) / 2)) + const nextBounds = { + left: imageLeft, + top: imageTop, + width: imageDisplayWidth, + height: imageDisplayHeight, + } + + if (options?.transformObjects && prevBounds.width > 0 && prevBounds.height > 0) { + const scaleX = nextBounds.width / prevBounds.width + const scaleY = nextBounds.height / prevBounds.height + canvas.getObjects().forEach((object) => { + const left = typeof object.left === 'number' ? object.left : 0 + const top = typeof object.top === 'number' ? object.top : 0 + object.set({ + left: nextBounds.left + (left - prevBounds.left) * scaleX, + top: nextBounds.top + (top - prevBounds.top) * scaleY, + scaleX: (object.scaleX ?? 1) * scaleX, + scaleY: (object.scaleY ?? 1) * scaleY, + }) + object.setCoords() + }) + const current = canvas.getActiveObject() ?? null + setActiveObject(current) + syncTextStyleFromObject(current) + } + + displaySizeRef.current = { width: stageWidth, height: stageHeight } + imageBoundsRef.current = nextBounds + exportMultiplierRef.current = Math.max(1, image.naturalWidth / Math.max(1, imageDisplayWidth)) + + backgroundElement.width = stageWidth + backgroundElement.height = stageHeight + canvas.setDimensions({ width: stageWidth, height: stageHeight }) + canvas.calcOffset() + + if (!options?.preserveViewport) { + zoomRef.current = 1 + panRef.current = { x: 0, y: 0 } + } + canvas.setViewportTransform([zoomRef.current, 0, 0, zoomRef.current, panRef.current.x, panRef.current.y]) + canvas.requestRenderAll() + redrawBackground() + }, [redrawBackground, syncTextStyleFromObject]) + + const syncViewportTransform = useCallback(() => { + const canvas = canvasRef.current + if (canvas) { + canvas.setViewportTransform([zoomRef.current, 0, 0, zoomRef.current, panRef.current.x, panRef.current.y]) + canvas.requestRenderAll() + } + redrawBackground() + }, [redrawBackground]) + + const createBaseImageProxy = useCallback(() => { + const bounds = imageBoundsRef.current + const proxy = new Rect({ + left: bounds.left + bounds.width / 2, + top: bounds.top + bounds.height / 2, + width: bounds.width, + height: bounds.height, + originX: 'center', + originY: 'center', + fill: 'rgba(59, 130, 246, 0.001)', + stroke: 'rgba(59, 130, 246, 0)', + strokeWidth: 0, + selectable: false, + evented: false, + hasControls: false, + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + lockScalingX: true, + lockScalingY: true, + hoverCursor: 'pointer', + }) + ;(proxy as FabricObject & { data?: BaseImageData }).data = { + editorKind: 'base-image', + flipX: baseFlipRef.current.x, + flipY: baseFlipRef.current.y, + } + return proxy + }, []) + + const syncBaseProxyData = useCallback(() => { + const canvas = canvasRef.current + if (!canvas) return + const proxy = canvas.getObjects().find((object) => getEditorKind(object) === 'base-image') as FabricObject | undefined + if (!proxy) return + ;(proxy as FabricObject & { data?: BaseImageData }).data = { + editorKind: 'base-image', + flipX: baseFlipRef.current.x, + flipY: baseFlipRef.current.y, + } + }, []) + + const addImageLayer = useCallback(async (dataUrl: string) => { + const canvas = canvasRef.current + if (!canvas) return + + const htmlImage = await loadHtmlImage(dataUrl) + const image = new FabricImage(htmlImage, { + width: htmlImage.naturalWidth, + height: htmlImage.naturalHeight, + left: canvas.getWidth() / 2, + top: canvas.getHeight() / 2, + originX: 'center', + originY: 'center', + cornerStyle: 'circle', + transparentCorners: false, + borderColor: '#3b82f6', + cornerColor: '#ffffff', + cornerStrokeColor: '#3b82f6', + }) + + const width = displaySizeRef.current.width || canvas.getWidth() + const height = displaySizeRef.current.height || canvas.getHeight() + const fitScale = Math.min( + 1, + (width * 0.55) / Math.max(1, htmlImage.naturalWidth), + (height * 0.55) / Math.max(1, htmlImage.naturalHeight), + ) + image.scale(Math.max(0.08, fitScale)) + image.setControlsVisibility({ mtr: true }) + canvas.add(image) + canvas.setActiveObject(image) + canvas.renderAll() + pushHistory() + }, [pushHistory]) + + useEffect(() => { + let disposed = false + + const mountCanvas = async () => { + const element = canvasElementRef.current + const backgroundElement = backgroundCanvasRef.current + const viewport = canvasViewportRef.current + if (!element || !backgroundElement || !viewport) return + + const htmlImage = await loadHtmlImage(src) + if (disposed) return + sourceImageRef.current = htmlImage + + const stageRect = viewport.getBoundingClientRect() + const stageWidth = Math.max(320, Math.floor(stageRect.width)) + const stageHeight = Math.max(320, Math.floor(stageRect.height)) + const imageScale = Math.min( + stageWidth / Math.max(1, htmlImage.naturalWidth), + stageHeight / Math.max(1, htmlImage.naturalHeight), + 1, + ) + const imageDisplayWidth = Math.max(1, Math.round(htmlImage.naturalWidth * imageScale)) + const imageDisplayHeight = Math.max(1, Math.round(htmlImage.naturalHeight * imageScale)) + const imageLeft = Math.max(0, Math.round((stageWidth - imageDisplayWidth) / 2)) + const imageTop = Math.max(0, Math.round((stageHeight - imageDisplayHeight) / 2)) + + displaySizeRef.current = { width: stageWidth, height: stageHeight } + sourceSizeRef.current = { + width: htmlImage.naturalWidth, + height: htmlImage.naturalHeight, + } + imageBoundsRef.current = { + left: imageLeft, + top: imageTop, + width: imageDisplayWidth, + height: imageDisplayHeight, + } + exportMultiplierRef.current = Math.max(1, htmlImage.naturalWidth / Math.max(1, imageDisplayWidth)) + zoomRef.current = 1 + panRef.current = { x: 0, y: 0 } + + backgroundElement.width = stageWidth + backgroundElement.height = stageHeight + + const canvas = new Canvas(element, { + selection: true, + preserveObjectStacking: true, + enableRetinaScaling: false, + }) + canvasRef.current = canvas + + canvas.setDimensions({ + width: stageWidth, + height: stageHeight, + }) + canvas.calcOffset() + canvas.add(createBaseImageProxy()) + redrawBackground() + + canvas.on('mouse:wheel', (event) => { + const wheelEvent = event.e as WheelEvent + wheelEvent.preventDefault() + wheelEvent.stopPropagation() + const nextZoom = clamp(zoomRef.current * Math.pow(0.999, wheelEvent.deltaY), 0.35, 6) + const factor = nextZoom / zoomRef.current + panRef.current = { + x: wheelEvent.offsetX - factor * (wheelEvent.offsetX - panRef.current.x), + y: wheelEvent.offsetY - factor * (wheelEvent.offsetY - panRef.current.y), + } + zoomRef.current = nextZoom + syncViewportTransform() + }) + + canvas.on('mouse:down', (event) => { + const rawEvent = event.e as MouseEvent + if (rawEvent.button === 1 || (spacePressedRef.current && toolModeRef.current === 'select')) { + panningRef.current = true + lastPointerRef.current = { x: rawEvent.clientX, y: rawEvent.clientY } + canvas.selection = false + canvas.defaultCursor = 'grab' + return + } + + if (toolModeRef.current === 'select' && !event.target) { + const pointer = canvas.getScenePoint(event.e) + const bounds = imageBoundsRef.current + const isInsideBaseImage = + pointer.x >= bounds.left && + pointer.x <= bounds.left + bounds.width && + pointer.y >= bounds.top && + pointer.y <= bounds.top + bounds.height + if (isInsideBaseImage) { + const baseProxy = canvas.getObjects().find((object) => getEditorKind(object) === 'base-image') ?? null + canvas.discardActiveObject() + canvas.requestRenderAll() + setActiveObject(baseProxy) + } + } + }) + + canvas.on('mouse:move', (event) => { + if (!panningRef.current) return + const rawEvent = event.e as MouseEvent + const deltaX = rawEvent.clientX - lastPointerRef.current.x + const deltaY = rawEvent.clientY - lastPointerRef.current.y + panRef.current = { + x: panRef.current.x + deltaX, + y: panRef.current.y + deltaY, + } + syncViewportTransform() + lastPointerRef.current = { x: rawEvent.clientX, y: rawEvent.clientY } + }) + + canvas.on('mouse:up', () => { + panningRef.current = false + canvas.selection = toolModeRef.current === 'select' + canvas.defaultCursor = toolModeRef.current === 'mask-brush' ? 'crosshair' : 'default' + }) + + const handleSelection = () => { + const current = canvas.getActiveObject() ?? null + setActiveObject(current) + syncTextStyleFromObject(current) + const selectedShapeType = getMaskShapeTypeFromObject(current) + if (selectedShapeType) { + setMaskShapeType(selectedShapeType) + if (typeof current?.fill === 'string') { + const match = current.fill.match(/hsla?\((\d+)/i) + if (match) { + setMaskHue(clamp(Number(match[1]) || DEFAULT_MASK_HUE, 0, 360)) + } + } + } + } + + canvas.on('selection:created', handleSelection) + canvas.on('selection:updated', handleSelection) + canvas.on('selection:cleared', () => { + setActiveObject(null) + }) + + const recordChange = () => pushHistory() + canvas.on('object:added', recordChange) + canvas.on('object:modified', recordChange) + canvas.on('object:removed', recordChange) + canvas.on('path:created', (event) => { + const path = event.path + if (path) { + ;(path as FabricObject & { data?: { editorKind: string } }).data = { editorKind: 'mask-brush' } + } + pushHistory() + }) + + refreshBrush() + suppressHistoryRef.current = true + historyRef.current = [] + historyIndexRef.current = -1 + suppressHistoryRef.current = false + pushHistory() + setReady(true) + } + + void mountCanvas() + + return () => { + disposed = true + setReady(false) + canvasRef.current?.dispose() + canvasRef.current = null + } + }, [createBaseImageProxy, pushHistory, redrawBackground, src, syncTextStyleFromObject, syncViewportTransform]) + + useEffect(() => { + const viewport = canvasViewportRef.current + if (!viewport) return + + let frame = 0 + const resize = () => { + window.cancelAnimationFrame(frame) + frame = window.requestAnimationFrame(() => { + resizeStageToViewport({ transformObjects: true }) + }) + } + const observer = new ResizeObserver(resize) + observer.observe(viewport) + window.addEventListener('orientationchange', resize) + return () => { + window.cancelAnimationFrame(frame) + observer.disconnect() + window.removeEventListener('orientationchange', resize) + } + }, [resizeStageToViewport]) + + useEffect(() => { + refreshBrush() + }, [refreshBrush]) + + useEffect(() => { + const viewport = canvasViewportRef.current + if (!viewport) return + + const getTouchMetrics = (event: TouchEvent) => { + const [first, second] = [event.touches[0], event.touches[1]] + const rect = viewport.getBoundingClientRect() + return { + distance: Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY), + centerX: (first.clientX + second.clientX) / 2 - rect.left, + centerY: (first.clientY + second.clientY) / 2 - rect.top, + } + } + + const onTouchStart = (event: TouchEvent) => { + if (event.touches.length !== 2) return + event.preventDefault() + const metrics = getTouchMetrics(event) + touchGestureRef.current = { + active: true, + distance: metrics.distance, + zoom: zoomRef.current, + panX: panRef.current.x, + panY: panRef.current.y, + centerX: metrics.centerX, + centerY: metrics.centerY, + } + } + + const onTouchMove = (event: TouchEvent) => { + if (!touchGestureRef.current.active || event.touches.length !== 2) return + event.preventDefault() + const metrics = getTouchMetrics(event) + const start = touchGestureRef.current + const nextZoom = clamp(start.zoom * (metrics.distance / Math.max(1, start.distance)), 0.35, 6) + const factor = nextZoom / Math.max(0.01, start.zoom) + zoomRef.current = nextZoom + panRef.current = { + x: metrics.centerX - factor * (start.centerX - start.panX), + y: metrics.centerY - factor * (start.centerY - start.panY), + } + syncViewportTransform() + } + + const onTouchEnd = (event: TouchEvent) => { + if (event.touches.length < 2) { + touchGestureRef.current.active = false + } + } + + viewport.addEventListener('touchstart', onTouchStart, { passive: false }) + viewport.addEventListener('touchmove', onTouchMove, { passive: false }) + viewport.addEventListener('touchend', onTouchEnd) + viewport.addEventListener('touchcancel', onTouchEnd) + return () => { + viewport.removeEventListener('touchstart', onTouchStart) + viewport.removeEventListener('touchmove', onTouchMove) + viewport.removeEventListener('touchend', onTouchEnd) + viewport.removeEventListener('touchcancel', onTouchEnd) + } + }, [syncViewportTransform]) + + useEffect(() => { + const canvas = canvasRef.current + const current = activeObject + if (!canvas || !current || getEditorKind(current) !== 'mask-region') return + current.set({ + fill: buildMaskColor(maskHue, maskOpacity), + }) + current.setCoords() + canvas.renderAll() + }, [activeObject, maskHue, maskOpacity]) + + const handleDeleteActiveObject = useCallback(() => { + const canvas = canvasRef.current + const current = canvas?.getActiveObject() + if (!canvas || !current) return + + const selection = current as FabricObject & { getObjects?: () => FabricObject[] } + if (typeof selection.getObjects === 'function') { + selection.getObjects().forEach((object) => { + if (getEditorKind(object) !== 'base-image') { + canvas.remove(object) + } + }) + } else if (getEditorKind(current) === 'base-image') { + return + } else { + canvas.remove(current) + } + canvas.discardActiveObject() + canvas.renderAll() + setActiveObject(null) + pushHistory() + }, [pushHistory]) + + useEffect(() => { + const onPaste = async (event: ClipboardEvent) => { + const items = Array.from(event.clipboardData?.items ?? []) + const imageItem = items.find((item) => item.type.startsWith('image/')) + if (!imageItem) return + const file = imageItem.getAsFile() + if (!file) return + event.preventDefault() + await addImageLayer(await readFileAsDataUrl(file)) + } + + const onKeyDown = async (event: KeyboardEvent) => { + if ((event.key === 'Delete' || event.key === 'Backspace') && activeObject) { + const target = event.target + const isTextInput = + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + (target instanceof HTMLElement && target.isContentEditable) + const current = canvasRef.current?.getActiveObject() + if (isTextInput || (current instanceof IText && current.isEditing)) { + return + } + handleDeleteActiveObject() + return + } + + const mod = event.ctrlKey || event.metaKey + if (mod && event.key.toLowerCase() === 'z' && !event.shiftKey) { + event.preventDefault() + if (historyIndexRef.current > 0) { + await restoreHistory(historyIndexRef.current - 1) + } + } + + if (mod && (event.key.toLowerCase() === 'y' || (event.shiftKey && event.key.toLowerCase() === 'z'))) { + event.preventDefault() + if (historyIndexRef.current < historyRef.current.length - 1) { + await restoreHistory(historyIndexRef.current + 1) + } + } + } + + const onKeyPressState = (event: KeyboardEvent, pressed: boolean) => { + if (event.code === 'Space') { + spacePressedRef.current = pressed + if (pressed) { + event.preventDefault() + } + } + } + + const onKeyDownState = (event: KeyboardEvent) => onKeyPressState(event, true) + const onKeyUpState = (event: KeyboardEvent) => onKeyPressState(event, false) + + window.addEventListener('paste', onPaste) + window.addEventListener('keydown', onKeyDown) + window.addEventListener('keydown', onKeyDownState) + window.addEventListener('keyup', onKeyUpState) + return () => { + window.removeEventListener('paste', onPaste) + window.removeEventListener('keydown', onKeyDown) + window.removeEventListener('keydown', onKeyDownState) + window.removeEventListener('keyup', onKeyUpState) + } + }, [activeObject, addImageLayer, handleDeleteActiveObject, restoreHistory]) + + const handleAddText = () => { + const canvas = canvasRef.current + if (!canvas) return + + const text = new IText(textStyle.text || DEFAULT_TEXT_STYLE.text, { + left: canvas.getWidth() / 2, + top: canvas.getHeight() / 2, + originX: 'center', + originY: 'center', + fill: textStyle.fill, + fontSize: textStyle.fontSize, + fontWeight: textStyle.fontWeight, + fontStyle: textStyle.fontStyle, + cornerStyle: 'circle', + transparentCorners: false, + borderColor: '#3b82f6', + cornerColor: '#ffffff', + cornerStrokeColor: '#3b82f6', + }) + + canvas.add(text) + canvas.setActiveObject(text) + canvas.renderAll() + text.enterEditing() + text.selectAll() + pushHistory() + } + + const handleObjectStyleChange = (patch: Partial) => { + const canvas = canvasRef.current + const current = canvas?.getActiveObject() + const next = { ...textStyle, ...patch } + setTextStyle(next) + + if (!(current instanceof IText) || !canvas) return + + current.set({ + text: next.text, + fill: next.fill, + fontSize: next.fontSize, + fontWeight: next.fontWeight, + fontStyle: next.fontStyle, + }) + current.setCoords() + canvas.renderAll() + pushHistory() + } + + const buildMaskShape = useCallback((shapeType: MaskShapeType, left: number, top: number, width: number, height: number) => { + const fill = buildMaskColor(maskHue, maskOpacity) + const centerLeft = left + width / 2 + const centerTop = top + height / 2 + const shared = { + left: centerLeft, + top: centerTop, + originX: 'center' as const, + originY: 'center' as const, + fill, + cornerStyle: 'circle' as const, + transparentCorners: false, + borderColor: '#3b82f6', + cornerColor: '#ffffff', + cornerStrokeColor: '#3b82f6', + } + + if (shapeType === 'ellipse') { + return new Ellipse({ + ...shared, + rx: width / 2, + ry: height / 2, + }) + } + + if (shapeType === 'triangle') { + return new Triangle({ + ...shared, + width, + height, + }) + } + + return new Rect({ + ...shared, + width, + height, + }) + }, [maskHue, maskOpacity]) + + const handleAddMaskRegion = () => { + const canvas = canvasRef.current + if (!canvas) return + + const mask = buildMaskShape( + maskShapeType, + canvas.getWidth() / 2 - 140, + canvas.getHeight() / 2 - 70, + 280, + 140, + ) + ;(mask as FabricObject & { data?: { editorKind: string; shapeType: MaskShapeType } }).data = { + editorKind: 'mask-region', + shapeType: maskShapeType, + } + canvas.add(mask) + canvas.setActiveObject(mask) + canvas.renderAll() + pushHistory() + } + + const handleChangeMaskShapeType = (nextShapeType: MaskShapeType) => { + setMaskShapeType(nextShapeType) + const canvas = canvasRef.current + if (!canvas || getEditorKind(activeObject) !== 'mask-region' || !activeObject) return + + const center = activeObject.getCenterPoint() + const scaledWidth = activeObject.getScaledWidth() + const scaledHeight = activeObject.getScaledHeight() + const angle = activeObject.angle ?? 0 + const fill = typeof activeObject.fill === 'string' + ? activeObject.fill + : buildMaskColor(maskHue, maskOpacity) + + suppressHistoryRef.current = true + canvas.remove(activeObject) + const replacement = buildMaskShape( + nextShapeType, + center.x - scaledWidth / 2, + center.y - scaledHeight / 2, + scaledWidth, + scaledHeight, + ) + replacement.set({ + angle, + fill, + }) + replacement.setPositionByOrigin(center, 'center', 'center') + ;(replacement as FabricObject & { data?: { editorKind: string; shapeType: MaskShapeType } }).data = { + editorKind: 'mask-region', + shapeType: nextShapeType, + } + canvas.add(replacement) + canvas.setActiveObject(replacement) + canvas.renderAll() + suppressHistoryRef.current = false + setActiveObject(replacement) + pushHistory() + } + + const handleFlipActiveObject = (axis: 'x' | 'y') => { + const canvas = canvasRef.current + const current = canvas?.getActiveObject() ?? activeObject + if (!canvas || !current) return + + if (getEditorKind(current) === 'base-image') { + if (axis === 'x') { + baseFlipRef.current = { ...baseFlipRef.current, x: !baseFlipRef.current.x } + } else { + baseFlipRef.current = { ...baseFlipRef.current, y: !baseFlipRef.current.y } + } + syncBaseProxyData() + redrawBackground() + canvas.requestRenderAll() + pushHistory() + return + } + + current.set(axis === 'x' ? { flipX: !current.flipX } : { flipY: !current.flipY }) + current.setCoords() + canvas.requestRenderAll() + pushHistory() + } + + const handleFileSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + event.target.value = '' + await addImageLayer(await readFileAsDataUrl(file)) + } + + const handleUndo = async () => { + if (historyIndexRef.current <= 0) return + await restoreHistory(historyIndexRef.current - 1) + } + + const handleRedo = async () => { + if (historyIndexRef.current >= historyRef.current.length - 1) return + await restoreHistory(historyIndexRef.current + 1) + } + + const handleSave = async () => { + const canvas = canvasRef.current + const sourceImage = sourceImageRef.current + if (!canvas || !sourceImage) return + + const previousViewport = canvas.viewportTransform ? [...canvas.viewportTransform] : null + const baseProxy = canvas.getObjects().find((object) => getEditorKind(object) === 'base-image') + const previousBaseVisible = baseProxy?.visible + if (baseProxy) { + baseProxy.set({ visible: false }) + } + canvas.setViewportTransform([1, 0, 0, 1, 0, 0]) + canvas.renderAll() + const overlayDataUrl = canvas.toDataURL({ + format: 'png', + left: imageBoundsRef.current.left, + top: imageBoundsRef.current.top, + width: imageBoundsRef.current.width, + height: imageBoundsRef.current.height, + multiplier: exportMultiplierRef.current, + }) + if (baseProxy) { + baseProxy.set({ visible: previousBaseVisible ?? true }) + } + if (previousViewport) { + canvas.setViewportTransform(previousViewport as [number, number, number, number, number, number]) + canvas.renderAll() + } + + const overlayImage = await loadHtmlImage(overlayDataUrl) + const exportCanvas = document.createElement('canvas') + exportCanvas.width = sourceSizeRef.current.width + exportCanvas.height = sourceSizeRef.current.height + const exportContext = exportCanvas.getContext('2d') + if (!exportContext) return + exportContext.imageSmoothingEnabled = true + exportContext.imageSmoothingQuality = 'high' + if (baseFlipRef.current.x || baseFlipRef.current.y) { + exportContext.translate(exportCanvas.width / 2, exportCanvas.height / 2) + exportContext.scale(baseFlipRef.current.x ? -1 : 1, baseFlipRef.current.y ? -1 : 1) + exportContext.drawImage(sourceImage, -exportCanvas.width / 2, -exportCanvas.height / 2, exportCanvas.width, exportCanvas.height) + exportContext.setTransform(1, 0, 0, 1, 0, 0) + } else { + exportContext.drawImage(sourceImage, 0, 0, exportCanvas.width, exportCanvas.height) + } + exportContext.drawImage(overlayImage, 0, 0, exportCanvas.width, exportCanvas.height) + const editedDataUrl = exportCanvas.toDataURL('image/png') + + if (saveMode === 'replace-input') { + const nextId = await replaceInputImageWithDataUrl(imageId, editedDataUrl) + setLightboxImageId(nextId, currentImageList.map((id) => (id === imageId ? nextId : id))) + onSaved?.(nextId, editedDataUrl) + showToast('参考图已更新', 'success') + } else { + const nextId = await addInputImageWithDataUrl(editedDataUrl) + const nextList = currentImageList.includes(nextId) ? currentImageList : [...currentImageList, nextId] + setLightboxImageId(nextId, nextList) + onSaved?.(nextId, editedDataUrl) + showToast('编辑结果已加入参考图', 'success') + } + onClose() + } + + const isTextSelected = activeObject instanceof IText + const activeEditorKind = getEditorKind(activeObject) + const canDeleteActiveObject = Boolean(activeObject && activeEditorKind !== 'base-image') + const isMaskRegionSelected = activeEditorKind === 'mask-region' + const showMaskSettings = toolMode === 'mask-brush' || isMaskRegionSelected + const settingsPanel = isTextSelected ? 'text' : showMaskSettings ? 'mask' : null + const showTextSettings = settingsPanel === 'text' + const showLocalMaskSettings = settingsPanel === 'mask' + + return ( +
+
+
+ {!ready && ( +
+ 正在加载编辑器... +
+ )} + + +
+