diff --git a/.gitignore b/.gitignore
index 676d43a..c727651 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ others/
public/style.css
public/highlight.css
public/tiptap
+public/lightbox
/static
.env
docs/superpowers/
diff --git a/Dioxus.toml b/Dioxus.toml
index 7abe9e8..94fff66 100644
--- a/Dioxus.toml
+++ b/Dioxus.toml
@@ -10,9 +10,9 @@ title = "Yggdrasil - Dioxus SSR"
watch_path = ["src", "Cargo.toml"]
[web.resource]
-style = ["/style.css", "/highlight.css", "/tiptap/editor.css"]
-script = ["/tiptap/editor.js"]
+style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css"]
+script = ["/tiptap/editor.js", "/lightbox/lightbox.js"]
[web.resource.dev]
-style = ["/style.css", "/highlight.css", "/tiptap/editor.css"]
-script = ["/tiptap/editor.js"]
+style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css"]
+script = ["/tiptap/editor.js", "/lightbox/lightbox.js"]
diff --git a/Makefile b/Makefile
index 94afbc1..7eab57f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,15 @@
-.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental highlight-css test
+.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental highlight-css test
build:
@$(MAKE) build-editor
+ @$(MAKE) build-lightbox
@$(MAKE) highlight-css
@tailwindcss -i input.css -o public/style.css --minify
@dx build --release --debug-symbols=false
build-linux:
@$(MAKE) build-editor
+ @$(MAKE) build-lightbox
@$(MAKE) highlight-css
@tailwindcss -i input.css -o public/style.css --minify
@dx build @client --release --debug-symbols=false --wasm-js-cfg false
@@ -26,7 +28,16 @@ build-editor:
build-editor-incremental:
@cd libs/tiptap-editor && npm run build
-dev: build-editor-incremental
+build-lightbox:
+ @echo "Building Lightbox..."
+ @cd libs/lightbox && npm install && npm run build
+ @echo "Lightbox built."
+
+# dev 用的增量构建:跳过 npm ci(假设 node_modules 已存在),仅 vite build。
+build-lightbox-incremental:
+ @cd libs/lightbox && npm run build
+
+dev: build-editor-incremental build-lightbox-incremental
@echo "Cleaning static/..."
@rm -rf static/
@echo "Building Tiptap editor (incremental)..."
@@ -45,6 +56,7 @@ css-watch:
test:
@cargo test
@cd libs/tiptap-editor && npm test
+ @cd libs/lightbox && npm test
clean:
@cargo clean
diff --git a/input.css b/input.css
index 1ffaf9c..0d7a893 100644
--- a/input.css
+++ b/input.css
@@ -526,78 +526,6 @@
text-underline-offset: 0.3rem;
}
- /* ========== 统一灯箱(Medium 风格) ========== */
- .lightbox-overlay {
- position: fixed;
- inset: 0;
- z-index: 50;
- /* overflow:hidden 裁剪超大的 absolute img,防止原图(可能数千 px)
- 撑大文档可滚动区,否则会触发非预期的 scroll 事件。 */
- overflow: hidden;
- background: rgba(0, 0, 0, 0.92);
- opacity: 1;
- }
-
- /* 灯箱图片:绝对定位,尺寸/位置由 JS 的 transform 控制。
- transform-origin 为 top left,配合 translate+scale 实现原地缩放。
- 显式 width/height 由 JS 设置为原图尺寸,确保布局尺寸确定,
- transform 只做视觉缩放,不改变占位。 */
- .lightbox-img {
- position: absolute;
- left: 0;
- top: 0;
- transform-origin: top left;
- max-width: none;
- max-height: none;
- object-fit: contain;
- opacity: 1;
- }
-
- .lightbox-caption {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 2rem;
- text-align: center;
- color: rgba(255, 255, 255, 0.85);
- font-size: 0.95rem;
- padding: 0 2rem;
- pointer-events: none;
- z-index: 2;
- }
-
- .lightbox-counter {
- position: fixed;
- right: 1.5rem;
- bottom: 1.5rem;
- color: rgba(255, 255, 255, 0.7);
- font-size: 0.85rem;
- z-index: 2;
- }
-
- .lightbox-nav {
- position: fixed;
- top: 50%;
- transform: translateY(-50%);
- background: transparent;
- border: none;
- color: rgba(255, 255, 255, 0.7);
- font-size: 3rem;
- line-height: 1;
- width: 3rem;
- height: 5rem;
- cursor: pointer;
- z-index: 2;
- border-radius: 0.5rem;
- transition: color 0.2s ease-out, background 0.2s ease-out;
- }
- .lightbox-nav:hover {
- color: #fff;
- background: rgba(255, 255, 255, 0.1);
- }
- .lightbox-prev { left: 1rem; }
- .lightbox-next { right: 1rem; }
-
/* 卡片封面:blur-img 结构,固定 4:3 比例,hover 缩放 */
.post-card-cover-blur {
margin: 0;
diff --git a/libs/lightbox/.gitignore b/libs/lightbox/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/libs/lightbox/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/libs/lightbox/package-lock.json b/libs/lightbox/package-lock.json
new file mode 100644
index 0000000..a4532c9
--- /dev/null
+++ b/libs/lightbox/package-lock.json
@@ -0,0 +1,1290 @@
+{
+ "name": "@yggdrasil/lightbox",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@yggdrasil/lightbox",
+ "version": "1.0.0",
+ "devDependencies": {
+ "typescript": "^6.0.3",
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz",
+ "integrity": "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz",
+ "integrity": "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz",
+ "integrity": "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz",
+ "integrity": "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz",
+ "integrity": "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz",
+ "integrity": "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz",
+ "integrity": "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz",
+ "integrity": "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz",
+ "integrity": "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz",
+ "integrity": "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz",
+ "integrity": "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz",
+ "integrity": "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.5"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz",
+ "integrity": "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz",
+ "integrity": "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.1.2.tgz",
+ "integrity": "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.137.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.2",
+ "@rolldown/binding-darwin-arm64": "1.1.2",
+ "@rolldown/binding-darwin-x64": "1.1.2",
+ "@rolldown/binding-freebsd-x64": "1.1.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.2",
+ "@rolldown/binding-linux-arm64-musl": "1.1.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-gnu": "1.1.2",
+ "@rolldown/binding-linux-x64-musl": "1.1.2",
+ "@rolldown/binding-openharmony-arm64": "1.1.2",
+ "@rolldown/binding-wasm32-wasi": "1.1.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.2",
+ "@rolldown/binding-win32-x64-msvc": "1.1.2"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-8.1.0.tgz",
+ "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "~1.1.2",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/libs/lightbox/package.json b/libs/lightbox/package.json
new file mode 100644
index 0000000..b9cae50
--- /dev/null
+++ b/libs/lightbox/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@yggdrasil/lightbox",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc --noEmit && vite build",
+ "dev": "vite",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "test:watch": "vitest"
+ },
+ "devDependencies": {
+ "typescript": "^6.0.3",
+ "vite": "^8.1.0",
+ "vitest": "^4.1.9"
+ }
+}
diff --git a/libs/lightbox/src/geometry.test.ts b/libs/lightbox/src/geometry.test.ts
new file mode 100644
index 0000000..82e19d2
--- /dev/null
+++ b/libs/lightbox/src/geometry.test.ts
@@ -0,0 +1,86 @@
+import { describe, it, expect } from 'vitest';
+import { fitCentered, transformFor, originalUrl, type Rect } from './geometry';
+
+describe('fitCentered', () => {
+ // 大图受 maxW=vw*0.92 / maxH=vh*0.88 约束,scale 取较小者
+ it('宽图被视口宽度卡住(横向受限)', () => {
+ // 2000x1000 in 1000x800: maxW=920, maxH=704
+ // scale = min(920/2000, 704/1000, 1) = min(0.46, 0.704, 1) = 0.46
+ const r = fitCentered(2000, 1000, 1000, 800);
+ expect(r.w).toBeCloseTo(920, 5);
+ expect(r.h).toBeCloseTo(460, 5);
+ expect(r.x).toBeCloseTo((1000 - 920) / 2, 5); // 40
+ expect(r.y).toBeCloseTo((800 - 460) / 2, 5); // 170
+ });
+
+ it('高图被视口高度卡住(纵向受限)', () => {
+ // 1000x2000 in 1000x800: maxW=920, maxH=704
+ // scale = min(920/1000, 704/2000, 1) = min(0.92, 0.352, 1) = 0.352
+ const r = fitCentered(1000, 2000, 1000, 800);
+ expect(r.w).toBeCloseTo(352, 5);
+ expect(r.h).toBeCloseTo(704, 5);
+ });
+
+ it('小图不被放大(scale 钳到 1)', () => {
+ // 300x200 in 1000x800: scale = min(920/300, 704/200, 1) = 1
+ const r = fitCentered(300, 200, 1000, 800);
+ expect(r.w).toBe(300);
+ expect(r.h).toBe(200);
+ expect(r.x).toBeCloseTo((1000 - 300) / 2, 5); // 350
+ expect(r.y).toBeCloseTo((800 - 200) / 2, 5); // 300
+ });
+
+ it('正方形图按 maxW/maxH 中较小者缩放', () => {
+ // 1000x1000 in 1000x800: maxW=920, maxH=704
+ // scale = min(0.92, 0.704, 1) = 0.704
+ const r = fitCentered(1000, 1000, 1000, 800);
+ expect(r.w).toBeCloseTo(704, 5);
+ expect(r.h).toBeCloseTo(704, 5);
+ });
+});
+
+describe('transformFor', () => {
+ it('居中态(base=target):scale=1,translate 到目标左上角', () => {
+ const target: Rect = { x: 40, y: 170, w: 920, h: 460 };
+ const t = transformFor(target, 920, 460);
+ expect(t).toBe('translate(40px,170px) scale(1,1)');
+ });
+
+ it('缩小态(rect 比 base 小):scale<1', () => {
+ // 原图位置小,base=居中尺寸 920x460
+ const origin: Rect = { x: 100, y: 500, w: 400, h: 200 };
+ const t = transformFor(origin, 920, 460);
+ // 400/920 = 0.43478260869565216(JS 浮点完整精度)
+ expect(t).toBe('translate(100px,500px) scale(0.43478260869565216,0.43478260869565216)');
+ });
+
+ it('baseW=0 守卫:scale 守卫为 1(不产生 NaN/Infinity)', () => {
+ const rect: Rect = { x: 0, y: 0, w: 0, h: 0 };
+ const t = transformFor(rect, 0, 0);
+ expect(t).toBe('translate(0px,0px) scale(1,1)');
+ });
+
+ it('字符串格式为 translate(Xpx,Ypx) scale(SX,SY)', () => {
+ const rect: Rect = { x: 10, y: 20, w: 100, h: 50 };
+ const t = transformFor(rect, 200, 100);
+ expect(t).toMatch(/^translate\(\d+(\.\d+)?px,\d+(\.\d+)?px\) scale\(\d+(\.\d+)?,\d+(\.\d+)?\)$/);
+ });
+});
+
+describe('originalUrl', () => {
+ it('去 query string', () => {
+ expect(originalUrl('/uploads/x.webp?w=800')).toBe('/uploads/x.webp');
+ });
+
+ it('无 query 原样返回', () => {
+ expect(originalUrl('/uploads/x.webp')).toBe('/uploads/x.webp');
+ });
+
+ it('null 输入返回空串', () => {
+ expect(originalUrl(null)).toBe('');
+ });
+
+ it('空串输入返回空串', () => {
+ expect(originalUrl('')).toBe('');
+ });
+});
diff --git a/libs/lightbox/src/geometry.ts b/libs/lightbox/src/geometry.ts
new file mode 100644
index 0000000..7c1c64c
--- /dev/null
+++ b/libs/lightbox/src/geometry.ts
@@ -0,0 +1,48 @@
+export interface Rect {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+}
+
+// 计算图片在视口居中、contain 适配后的目标 rect。
+// naturalW/H: 图片真实像素尺寸;vw/vh: 视口尺寸。
+export function fitCentered(naturalW: number, naturalH: number, vw: number, vh: number): Rect {
+ var maxW = vw * 0.92;
+ var maxH = vh * 0.88;
+ var scale = Math.min(maxW / naturalW, maxH / naturalH, 1);
+ var w = naturalW * scale;
+ var h = naturalH * scale;
+ return {
+ x: (vw - w) / 2,
+ y: (vh - h) / 2,
+ w: w,
+ h: h,
+ };
+}
+
+// 把目标 rect 转成 transform 字符串。
+// baseW/baseH 是 img 元素的布局尺寸(=居中目标尺寸),scale 相对它缩放。
+// transform-origin 为 top left(见 CSS),translate 到 rect 左上角后 scale。
+// - 居中态:scale=1(base 就是居中尺寸)
+// - originRect 态:scale = originRect.w / base.w(缩小)
+export function transformFor(rect: Rect, baseW: number, baseH: number): string {
+ var sx = baseW > 0 ? rect.w / baseW : 1;
+ var sy = baseH > 0 ? rect.h / baseH : 1;
+ return (
+ "translate(" +
+ rect.x +
+ "px," +
+ rect.y +
+ "px) scale(" +
+ sx +
+ "," +
+ sy +
+ ")"
+ );
+}
+
+// 原图 URL = data-src 去 query。data-src 形如 "/uploads/x.webp?w=800"。
+export function originalUrl(dataSrc: string | null): string {
+ return (dataSrc || "").split("?")[0];
+}
diff --git a/public/js/lightbox.js b/libs/lightbox/src/index.ts
similarity index 74%
rename from public/js/lightbox.js
rename to libs/lightbox/src/index.ts
index d63f6b7..4a87bb7 100644
--- a/public/js/lightbox.js
+++ b/libs/lightbox/src/index.ts
@@ -1,11 +1,46 @@
+import { fitCentered, transformFor, originalUrl, type Rect } from "./geometry";
+import "./style.css";
+
+interface LightboxState {
+ overlay: HTMLDivElement;
+ img: HTMLImageElement;
+ caption: HTMLElement;
+ counter: HTMLDivElement;
+ prevBtn: HTMLButtonElement | null;
+ nextBtn: HTMLButtonElement | null;
+ originNode: HTMLElement;
+ gallery: HTMLElement[];
+ index: number | null;
+ isSingle: boolean;
+ openScrollY: number;
+ origSrc: string;
+ altText: string;
+ closing: boolean;
+ reduced: boolean;
+ scrollHandler: ((this: Window, ev: Event) => void) | null;
+ keyHandler: ((this: Document, ev: KeyboardEvent) => void) | null;
+ target?: Rect;
+ baseW?: number;
+ baseH?: number;
+}
+
+declare global {
+ interface Window {
+ __initLightbox: (selectors: string | string[]) => void;
+ __lightboxSelectors?: string[];
+ }
+}
+
+export {};
+
(function () {
"use strict";
// ============ 工具函数 ============
- function prefersReducedMotion() {
+ function prefersReducedMotion(): boolean {
return (
- window.matchMedia &&
+ !!window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
@@ -14,7 +49,7 @@
// 统一映射成 {x,y,w,h}:getBoundingClientRect 返回的 DOMRect 用
// left/top/width/height,而 fitCentered/transformFor 用 x/y/w/h,
// 这里转成一致格式,避免 .w 读到 undefined。
- function rectOf(el) {
+ function rectOf(el: Element): Rect {
var r = el.getBoundingClientRect();
return {
x: r.left,
@@ -24,62 +59,27 @@
};
}
- // 计算图片在视口居中、contain 适配后的目标 rect。
- // naturalW/H: 图片真实像素尺寸;vw/vh: 视口尺寸。
- function fitCentered(naturalW, naturalH, vw, vh) {
- var maxW = vw * 0.92;
- var maxH = vh * 0.88;
- var scale = Math.min(maxW / naturalW, maxH / naturalH, 1);
- var w = naturalW * scale;
- var h = naturalH * scale;
- return {
- x: (vw - w) / 2,
- y: (vh - h) / 2,
- w: w,
- h: h,
- };
- }
-
- // 把目标 rect 转成 transform 字符串。
- // baseW/baseH 是 img 元素的布局尺寸(=居中目标尺寸),scale 相对它缩放。
- // transform-origin 为 top left(见 CSS),translate 到 rect 左上角后 scale。
- // - 居中态:scale=1(base 就是居中尺寸)
- // - originRect 态:scale = originRect.w / base.w(缩小)
- function transformFor(rect, baseW, baseH) {
- var sx = baseW > 0 ? rect.w / baseW : 1;
- var sy = baseH > 0 ? rect.h / baseH : 1;
- return (
- "translate(" +
- rect.x +
- "px," +
- rect.y +
- "px) scale(" +
- sx +
- "," +
- sy +
- ")"
- );
- }
-
- // 原图 URL = data-src 去 query。data-src 形如 "/uploads/x.webp?w=800"。
- function originalUrl(dataSrc) {
- return (dataSrc || "").split("?")[0];
- }
+ // 注意:fitCentered / transformFor / originalUrl 已抽到 ./geometry.ts(见 Task 2)。
// ============ 懒加载 ============
// 为单个 .blur-img 容器初始化高清图懒加载。
// IO 进入视口后把 data-src 写入 src,加载完成加 is-loaded 触发 CSS 淡入。
- function initLazyLoad(container) {
- var fullImg = container.querySelector(".blur-img-full");
- if (!fullImg) return;
+ function initLazyLoad(container: Element): void {
+ var raw = container.querySelector(".blur-img-full");
+ if (!(raw instanceof HTMLImageElement)) return;
+ // 用 const + 显式类型锁住窄化结果:var 在闭包内会被放宽回 Element | null,
+ // 导致 onFullLoaded/IntersectionObserver 回调里访问 .style/.src 报错。
+ var fullImg: HTMLImageElement = raw;
if (container.getAttribute("data-blur-init")) return;
container.setAttribute("data-blur-init", "true");
- var fullSrc = fullImg.getAttribute("data-src");
- if (!fullSrc) return;
+ var rawSrc = fullImg.getAttribute("data-src");
+ if (!rawSrc) return;
+ // 同上:const 锁住 string 窄化,避免闭包内放宽回 string | null。
+ var fullSrc: string = rawSrc;
- var onFullLoaded = function () {
+ var onFullLoaded = function (): void {
// 给容器加 is-loaded,CSS 据此显式隐藏 placeholder。
// 直接把 full 层 opacity 设为 1(清掉 transition),不依赖 CSS 的 opacity
// 过渡:合成层重绘时机不稳定,可能导致 full 层卡在 opacity:0,直到一次
@@ -97,8 +97,8 @@
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
- function (entries) {
- entries.forEach(function (entry) {
+ function (entries: IntersectionObserverEntry[]): void {
+ entries.forEach(function (entry): void {
if (entry.isIntersecting) {
fullImg.src = fullSrc;
io.unobserve(container);
@@ -117,13 +117,14 @@
// 收集所有 selectors命中的 .blur-img 节点。
// gallery: 正文图(组成图集);singles: 带 lightbox-single class 的单张图(如封面)。
- function collectImages(roots) {
- var gallery = [];
- var singles = [];
+ function collectImages(roots: Element[]): { gallery: HTMLElement[]; singles: HTMLElement[] } {
+ var gallery: HTMLElement[] = [];
+ var singles: HTMLElement[] = [];
for (var i = 0; i < roots.length; i++) {
var nodes = roots[i].querySelectorAll(".blur-img");
for (var j = 0; j < nodes.length; j++) {
var n = nodes[j];
+ if (!(n instanceof HTMLElement)) continue;
if (n.classList.contains("lightbox-single")) {
singles.push(n);
} else {
@@ -137,16 +138,16 @@
// ============ 灯箱状态与开/关 ============
// 当前灯箱状态(同时只允许一个灯箱)。
- var state = null;
+ var state: LightboxState | null = null;
- function openLightbox(originNode, gallery, index) {
+ function openLightbox(originNode: HTMLElement, gallery: HTMLElement[], index: number | null): void {
if (state) closeLightbox(true);
- var fullImg = originNode.querySelector(".blur-img-full");
- if (!fullImg) return;
- var dataSrc = fullImg.getAttribute("data-src") || "";
+ var fullImgEl = originNode.querySelector(".blur-img-full");
+ if (!(fullImgEl instanceof HTMLImageElement)) return;
+ var dataSrc = fullImgEl.getAttribute("data-src") || "";
var origSrc = originalUrl(dataSrc);
- var altText = fullImg.getAttribute("alt") || "";
+ var altText = fullImgEl.getAttribute("alt") || "";
var isSingle =
originNode.classList.contains("lightbox-single") ||
gallery.length === 0;
@@ -183,12 +184,12 @@
if (isSingle || gallery.length === 0) {
counter.style.display = "none";
} else {
- counter.textContent = index + 1 + " / " + gallery.length;
+ counter.textContent = (index ?? 0) + 1 + " / " + gallery.length;
}
// 图集模式(>1 张)才加左右导航箭头;单张不显示。
- var prevBtn = null;
- var nextBtn = null;
+ var prevBtn: HTMLButtonElement | null = null;
+ var nextBtn: HTMLButtonElement | null = null;
if (!isSingle && gallery.length > 1) {
prevBtn = document.createElement("button");
prevBtn.className = "lightbox-nav lightbox-prev";
@@ -236,7 +237,7 @@
bindInteractions();
// 图片加载后再做动画(naturalW/H 要等加载)
- var start = function () {
+ var start = function (): void {
if (!state) return; // 加载前可能已被关闭
var naturalW = img.naturalWidth || img.clientWidth || 1;
var naturalH = img.naturalHeight || img.clientHeight || 1;
@@ -265,7 +266,7 @@
img.style.top = "0";
overlay.style.opacity = "0";
// 下一帧淡入
- requestAnimationFrame(function () {
+ requestAnimationFrame(function (): void {
if (!state) return;
overlay.style.transition = "opacity 200ms ease-out";
img.style.transition = "opacity 200ms ease-out";
@@ -287,9 +288,9 @@
void img.offsetHeight;
// double-rAF:第一帧绘制首帧(无动画),第二帧才启动 transition 到居中。
- requestAnimationFrame(function () {
+ requestAnimationFrame(function (): void {
if (!state) return;
- requestAnimationFrame(function () {
+ requestAnimationFrame(function (): void {
if (!state) return;
img.style.transition =
"transform 250ms ease-out, opacity 250ms ease-out";
@@ -309,7 +310,7 @@
img.src = origSrc;
}
- function closeLightbox(immediate) {
+ function closeLightbox(immediate: boolean): void {
if (!state || state.closing) return;
state.closing = true;
cleanupInteractions();
@@ -334,27 +335,29 @@
s.img.style.opacity = "0";
s.overlay.style.opacity = "0";
- var done = function () {
+ var done = function (): void {
removeOverlay();
};
// 250ms 兜底,避免 transitionend 不触发
var timer = setTimeout(done, 280);
- s.img.addEventListener("transitionend", function () { clearTimeout(timer); done(); }, { once: true });
+ s.img.addEventListener("transitionend", function (): void { clearTimeout(timer); done(); }, { once: true });
}
- function removeOverlay() {
+ function removeOverlay(): void {
if (!state) return;
var prev = state.originNode;
if (state.overlay && state.overlay.parentNode) {
state.overlay.parentNode.removeChild(state.overlay);
}
state = null;
- // 焦点归还:.blur-img 是 span 不可聚焦,让其内部 full img 获得焦点
+ // 焦点归还:.blur-img 是 span 不可聚焦,让其内部 full img 获得焦点。
+ // 用 preventScroll 抑制 focus() 默认的 scrollIntoView 行为——否则关闭灯箱后
+ // 页面会自动滚动把原图完整纳入视口(用户只点了一半露出的图时尤其明显)。
if (prev) {
var f = prev.querySelector(".blur-img-full");
- if (f) {
+ if (f instanceof HTMLImageElement) {
f.setAttribute("tabindex", "-1");
- f.focus();
+ f.focus({ preventScroll: true });
}
}
}
@@ -362,7 +365,7 @@
// ============ 图集切换 ============
// 图集切换:淡入淡出,不飞行。newIndex 循环(首尾衔接)。
- function gotoIndex(newIndex) {
+ function gotoIndex(newIndex: number): void {
if (!state || state.isSingle) return;
var s = state;
if (!s.gallery || s.gallery.length === 0) return;
@@ -371,19 +374,19 @@
if (newIndex === s.index) return;
var newNode = s.gallery[newIndex];
- var fullImg = newNode.querySelector(".blur-img-full");
- if (!fullImg) return;
- var origSrc = originalUrl(fullImg.getAttribute("data-src") || "");
- var altText = fullImg.getAttribute("alt") || "";
+ var fullImgEl = newNode.querySelector(".blur-img-full");
+ if (!(fullImgEl instanceof HTMLImageElement)) return;
+ var origSrc = originalUrl(fullImgEl.getAttribute("data-src") || "");
+ var altText = fullImgEl.getAttribute("alt") || "";
// 淡出当前图
s.img.style.transition = "opacity 150ms ease-out";
s.img.style.opacity = "0";
// 150ms 后换图淡入
- var swap = function () {
+ var swap = function (): void {
if (!state) return; // 切换中可能已关闭
- var fade = function () {
+ var fade = function (): void {
if (!state) return;
s.img.style.transition = "opacity 150ms ease-out";
s.img.style.opacity = "1";
@@ -409,18 +412,18 @@
// ============ 交互绑定 ============
- function bindInteractions() {
+ function bindInteractions(): void {
var s = state;
if (!s) return;
// 点背景关闭(点图片本身不关,因箭头在图上、避免误关)
- s.overlay.addEventListener("click", function (ev) {
+ s.overlay.addEventListener("click", function (ev: MouseEvent): void {
if (state && ev.target === state.overlay) closeLightbox(false);
});
// 滚动驱动关闭:任何 scroll 都触发,用 scrollY 偏移算进度。
// 关键:逐帧读 originNode 实时 rect,文章滚多少图就回多少。
- s.scrollHandler = function () {
+ s.scrollHandler = function (): void {
if (!state) return;
var st = state;
if (st.closing) return;
@@ -430,13 +433,14 @@
closeLightbox(true);
return;
}
- var progress = Math.min(dy / 120, 1);
var target = st.target;
var baseW = st.baseW || (target ? target.w : 1);
var baseH = st.baseH || (target ? target.h : 1);
var originRect = rectOf(st.originNode);
// 在 originRect 与居中 target 之间按 progress 线性插值
- var cur = {
+ var progress = Math.min(dy / 120, 1);
+ if (!target) return; // 无 target 时不插值(忠实原 JS 的兜底语义)
+ var cur: Rect = {
x: target.x + (originRect.x - target.x) * progress,
y: target.y + (originRect.y - target.y) * progress,
w: target.w + (originRect.w - target.w) * progress,
@@ -456,17 +460,17 @@
window.addEventListener("scroll", s.scrollHandler, { passive: true });
// 键盘:Esc 关;图集模式 ←→ 切换
- s.keyHandler = function (ev) {
+ s.keyHandler = function (ev: KeyboardEvent): void {
if (!state) return;
if (ev.key === "Escape") {
closeLightbox(false);
} else if (!state.isSingle && state.gallery.length > 1) {
if (ev.key === "ArrowLeft") {
ev.preventDefault();
- gotoIndex(state.index - 1);
+ gotoIndex((state.index ?? 0) - 1);
} else if (ev.key === "ArrowRight") {
ev.preventDefault();
- gotoIndex(state.index + 1);
+ gotoIndex((state.index ?? 0) + 1);
}
}
};
@@ -474,20 +478,20 @@
// 图集导航箭头点击(stopPropagation 防止冒泡到 overlay 触发关闭)
if (s.prevBtn) {
- s.prevBtn.addEventListener("click", function (ev) {
+ s.prevBtn.addEventListener("click", function (ev: MouseEvent): void {
ev.stopPropagation();
- if (state) gotoIndex(state.index - 1);
+ if (state) gotoIndex((state.index ?? 0) - 1);
});
}
if (s.nextBtn) {
- s.nextBtn.addEventListener("click", function (ev) {
+ s.nextBtn.addEventListener("click", function (ev: MouseEvent): void {
ev.stopPropagation();
- if (state) gotoIndex(state.index + 1);
+ if (state) gotoIndex((state.index ?? 0) + 1);
});
}
}
- function cleanupInteractions() {
+ function cleanupInteractions(): void {
if (!state) return;
if (state.scrollHandler) {
window.removeEventListener("scroll", state.scrollHandler);
@@ -501,10 +505,10 @@
// ============ 初始化入口 ============
- window.__initLightbox = function (selectors) {
+ window.__initLightbox = function (selectors: string | string[]): void {
// selectors 可以是字符串、字符串数组
var sels = Array.isArray(selectors) ? selectors : [selectors];
- var roots = [];
+ var roots: Element[] = [];
for (var i = 0; i < sels.length; i++) {
var found = document.querySelectorAll(sels[i]);
for (var j = 0; j < found.length; j++) roots.push(found[j]);
@@ -520,8 +524,8 @@
// 正文图:带 index
var gallery = collected.gallery;
for (var g = 0; g < gallery.length; g++) {
- (function (node, idx) {
- node.addEventListener("click", function (e) {
+ (function (node: HTMLElement, idx: number): void {
+ node.addEventListener("click", function (e: MouseEvent): void {
e.preventDefault();
openLightbox(node, gallery, idx);
});
@@ -529,12 +533,22 @@
}
// 单张图(封面):index = null,gallery 传空数组表示单张
for (var si = 0; si < collected.singles.length; si++) {
- (function (node) {
- node.addEventListener("click", function (e) {
+ (function (node: HTMLElement): void {
+ node.addEventListener("click", function (e: MouseEvent): void {
e.preventDefault();
openLightbox(node, [], null);
});
})(collected.singles[si]);
}
};
+
+ // ============ 自启动 ============
+ // 方案 iii:双保险契约,无需轮询。
+ // 1) Rust 内联 eval 先跑(常态):设 __lightboxSelectors,此时 __initLightbox 可能未定义 → 只设配置;
+ // lightbox.js 后加载完 → 读到配置 → 这里自启动。
+ // 2) lightbox.js 先加载完:__initLightbox 就绪但无配置 → 不自启动;
+ // Rust eval 后跑 → 设配置 + 兜底 if(__initLightbox) 显式调用 → 初始化。
+ if (Array.isArray(window.__lightboxSelectors)) {
+ window.__initLightbox(window.__lightboxSelectors);
+ }
})();
diff --git a/libs/lightbox/src/style.css b/libs/lightbox/src/style.css
new file mode 100644
index 0000000..d40aeec
--- /dev/null
+++ b/libs/lightbox/src/style.css
@@ -0,0 +1,71 @@
+/* ========== 统一灯箱(Medium 风格) ========== */
+.lightbox-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ /* overflow:hidden 裁剪超大的 absolute img,防止原图(可能数千 px)
+ 撑大文档可滚动区,否则会触发非预期的 scroll 事件。 */
+ overflow: hidden;
+ background: rgba(0, 0, 0, 0.92);
+ opacity: 1;
+}
+
+/* 灯箱图片:绝对定位,尺寸/位置由 JS 的 transform 控制。
+ transform-origin 为 top left,配合 translate+scale 实现原地缩放。
+ 显式 width/height 由 JS 设置为原图尺寸,确保布局尺寸确定,
+ transform 只做视觉缩放,不改变占位。 */
+.lightbox-img {
+ position: absolute;
+ left: 0;
+ top: 0;
+ transform-origin: top left;
+ max-width: none;
+ max-height: none;
+ object-fit: contain;
+ opacity: 1;
+}
+
+.lightbox-caption {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 2rem;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.95rem;
+ padding: 0 2rem;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.lightbox-counter {
+ position: fixed;
+ right: 1.5rem;
+ bottom: 1.5rem;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.85rem;
+ z-index: 2;
+}
+
+.lightbox-nav {
+ position: fixed;
+ top: 50%;
+ transform: translateY(-50%);
+ background: transparent;
+ border: none;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 3rem;
+ line-height: 1;
+ width: 3rem;
+ height: 5rem;
+ cursor: pointer;
+ z-index: 2;
+ border-radius: 0.5rem;
+ transition: color 0.2s ease-out, background 0.2s ease-out;
+}
+.lightbox-nav:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.1);
+}
+.lightbox-prev { left: 1rem; }
+.lightbox-next { right: 1rem; }
diff --git a/libs/lightbox/src/vite-env.d.ts b/libs/lightbox/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/libs/lightbox/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/libs/lightbox/tsconfig.json b/libs/lightbox/tsconfig.json
new file mode 100644
index 0000000..fd15971
--- /dev/null
+++ b/libs/lightbox/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "types": []
+ },
+ "include": ["src"]
+}
diff --git a/libs/lightbox/vite.config.ts b/libs/lightbox/vite.config.ts
new file mode 100644
index 0000000..b6aa91b
--- /dev/null
+++ b/libs/lightbox/vite.config.ts
@@ -0,0 +1,23 @@
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+
+export default defineConfig({
+ build: {
+ outDir: resolve(__dirname, '../../public/lightbox'),
+ emptyOutDir: true,
+ lib: {
+ entry: resolve(__dirname, 'src/index.ts'),
+ name: 'Lightbox',
+ fileName: () => 'lightbox.js',
+ formats: ['iife'],
+ },
+ rolldownOptions: {
+ output: {
+ assetFileNames: 'lightbox.[ext]',
+ },
+ },
+ cssCodeSplit: false,
+ minify: true,
+ sourcemap: true,
+ },
+});
diff --git a/libs/lightbox/vitest.config.ts b/libs/lightbox/vitest.config.ts
new file mode 100644
index 0000000..7eeb3f8
--- /dev/null
+++ b/libs/lightbox/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+});
diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs
index 75fe4e8..032640a 100644
--- a/src/components/post/post_content.rs
+++ b/src/components/post/post_content.rs
@@ -10,17 +10,22 @@ use dioxus::prelude::*;
/// - `content_html`:服务端渲染的文章 HTML 字符串
///
/// 关键行为:
-/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)与
-/// `lightbox.js`(图片灯箱 + 懒加载),并调用各自的初始化函数。
+/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。
+/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`,
+/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
#[component]
pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")]
use_effect(move || {
let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
- let _ = js_sys::eval(include_str!("../../../public/js/lightbox.js"));
let _ = js_sys::eval("window.__initPostContent('.post-content')");
- // 正文图组成图集;封面(.entry-cover,带 lightbox-single class)单张模式。
- let _ = js_sys::eval("window.__initLightbox(['.post-content', '.entry-cover'])");
+ // lightbox 改由 Dioxus.toml 全局