Compare commits

..

No commits in common. "8ec4ecd31087eb2e05ee6025bdaa8e9fe0481b99" and "731547e6dfb490eca6a8adfedd0ab53f484fc669" have entirely different histories.

29 changed files with 3592 additions and 3660 deletions

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ public/style.css
public/highlight.css public/highlight.css
public/tiptap public/tiptap
public/lightbox public/lightbox
public/yggdrasil-core
public/doc public/doc
/static /static
.env .env

View File

@ -15,7 +15,7 @@ make build # build-editor → highlight-css → tailwindcss → doc →
make build-linux # same as build but targets x86_64-unknown-linux-musl make build-linux # same as build but targets x86_64-unknown-linux-musl
make css # one-shot CSS make css # one-shot CSS
make css-watch # watch mode make css-watch # watch mode
make test # cargo test + vitest (tiptap-editor + lightbox + yggdrasil-core libs) make test # cargo test + vitest (tiptap-editor + lightbox libs)
make doc # cargo doc (ayu 主题) → 拷贝到 public/doc/,随 build 发布 make doc # cargo doc (ayu 主题) → 拷贝到 public/doc/,随 build 发布
make doc-open # 同 doc生成后自动用浏览器打开本地预览不拷贝 make doc-open # 同 doc生成后自动用浏览器打开本地预览不拷贝
make clean # cargo clean + rm public/style.css make clean # cargo clean + rm public/style.css
@ -150,18 +150,6 @@ Image lightbox (click-to-zoom) in `libs/lightbox/`, built as an IIFE library. Un
Do not edit `public/lightbox/` — they are build artifacts. Do not edit `public/lightbox/` — they are build artifacts.
## Yggdrasil-Core Subproject
Core JavaScript bundle in `libs/yggdrasil-core/`, built as an IIFE library exposing `window.__initPostContent` and `window.__startThemeTransition` (and future core JS entry points). This is the designated home for all new core JS — add to it rather than creating new `public/js/` scripts.
- Output: `public/yggdrasil-core/` (`yggdrasil-core.js`, `yggdrasil-core.css`, `yggdrasil-core.js.map`)
- `make build` runs `npm ci --include=dev && npm run build` inside `libs/yggdrasil-core`; the `build` script is `tsc --noEmit && vite build` (Vite 8 / Rolldown, type-check before bundle). Source is ES module, output is IIFE (`formats: ['iife']` in `vite.config.ts`) because Dioxus `[web.resource] script` injects bare `<script src>` without `type="module"` support.
- Injected globally via `Dioxus.toml` (`script` + `style` arrays). Rust calls the entry points via `js_sys::eval("window.__xxx(...)")` with an `if (window.__xxx)` guard to survive script-load ordering races.
- Unit tests: `npm test` (Vitest 4 + happy-dom), covering `post-content` copy-button lifecycle and `theme-transition` fallback paths (happy-dom lacks `startViewTransition`, naturally covering the degradation path).
- Theme reveal animation uses the View Transitions API: JS synchronously toggles the `dark` class inside `startViewTransition`'s callback (so the browser snapshots the new state), CSS `@keyframes tt-reveal` expands `::view-transition-new(root)` via `clip-path: circle()` from the click point; Rust's `theme.set()` aligns state afterward (idempotent — `use_effect` sets the same class again). `prefers-reduced-motion` and browsers without VT fall back to instant switch.
Do not edit `public/yggdrasil-core/` — they are build artifacts.
## Syntax Highlighting Pipeline ## Syntax Highlighting Pipeline
- `themes/` contains Catppuccin Latte (light) and Mocha (dark) `.tmTheme` files - `themes/` contains Catppuccin Latte (light) and Mocha (dark) `.tmTheme` files
@ -185,7 +173,7 @@ Do not edit `public/yggdrasil-core/` — they are build artifacts.
## Testing ## Testing
```bash ```bash
make test # cargo test (Rust, 402 tests) + vitest (tiptap-editor 46 tests, lightbox 23 tests, yggdrasil-core 9 tests) make test # cargo test (Rust, 402 tests) + vitest (tiptap-editor 46 tests, lightbox 23 tests)
dx check # Dioxus type-check (catches component/Router issues) dx check # Dioxus type-check (catches component/Router issues)
cargo clippy # lint cargo clippy # lint
``` ```
@ -205,9 +193,8 @@ Most tests use `#[cfg(all(test, feature = "server"))]` — they only run when th
- `public/highlight.css` — generated by `generate_highlight_css` binary - `public/highlight.css` — generated by `generate_highlight_css` binary
- `public/tiptap/` — Vite build output (editor) - `public/tiptap/` — Vite build output (editor)
- `public/lightbox/` — Vite build output (lightbox) - `public/lightbox/` — Vite build output (lightbox)
- `public/yggdrasil-core/` — Vite build output (core JS bundle)
- `/dist`, `/.dioxus`, `/target` - `/dist`, `/.dioxus`, `/target`
- `node_modules` (inside `libs/tiptap-editor/`, `libs/lightbox/`, and `libs/yggdrasil-core/`) - `node_modules` (inside `libs/tiptap-editor/` and `libs/lightbox/`)
- `uploads/.cache/` — image processing disk cache - `uploads/.cache/` — image processing disk cache
## Notes ## Notes

View File

@ -10,9 +10,9 @@ title = "Yggdrasil - Dioxus SSR"
watch_path = ["src", "Cargo.toml"] watch_path = ["src", "Cargo.toml"]
[web.resource] [web.resource]
style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"] style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css"]
script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] script = ["/tiptap/editor.js", "/lightbox/lightbox.js"]
[web.resource.dev] [web.resource.dev]
style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"] style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css"]
script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"] script = ["/tiptap/editor.js", "/lightbox/lightbox.js"]

View File

@ -1,59 +1,43 @@
.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental highlight-css test doc doc-open start .PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental highlight-css test doc doc-open
build: build:
@$(MAKE) build-editor @$(MAKE) build-editor
@$(MAKE) build-lightbox @$(MAKE) build-lightbox
@$(MAKE) build-core
@$(MAKE) highlight-css @$(MAKE) highlight-css
@tailwindcss -i input.css -o public/style.css --minify @tailwindcss -i input.css -o public/style.css --minify
@$(MAKE) doc
@dx build --release --debug-symbols=false @dx build --release --debug-symbols=false
build-linux: build-linux:
@$(MAKE) build-editor @$(MAKE) build-editor
@$(MAKE) build-lightbox @$(MAKE) build-lightbox
@$(MAKE) build-core
@$(MAKE) highlight-css @$(MAKE) highlight-css
@tailwindcss -i input.css -o public/style.css --minify @tailwindcss -i input.css -o public/style.css --minify
@dx build @client --release --debug-symbols=false --wasm-js-cfg false @dx build @client --release --debug-symbols=false --wasm-js-cfg false
@dx build @server --release --debug-symbols=false --target x86_64-unknown-linux-musl --wasm-js-cfg false --features server @dx build @server --release --debug-symbols=false --target x86_64-unknown-linux-musl --wasm-js-cfg false --features server
@echo ""
@echo "Linux build complete! The server binary is at target/dx/yggdrasil/release/web/server"
@echo "Remember to deploy it alongside the target/dx/yggdrasil/release/web/public directory."
@echo "When running the server, ensure DIOXUS_ASSET_DIR is set or the public directory is in CWD."
highlight-css: highlight-css:
@cargo run --bin generate_highlight_css @cargo run --bin generate_highlight_css
build-editor: build-editor:
@echo "Building Tiptap editor..." @echo "Building Tiptap editor..."
@cd libs/tiptap-editor && pnpm ci --include=dev && pnpm run build @cd libs/tiptap-editor && npm ci --include=dev && npm run build
@echo "Tiptap editor built." @echo "Tiptap editor built."
# dev 用的增量构建:跳过 pnpm ci假设 node_modules 已存在),仅 vite build。 # dev 用的增量构建:跳过 npm ci假设 node_modules 已存在),仅 vite build。
# 与 build-editor 分开,避免每次 make dev 都重装依赖。 # 与 build-editor 分开,避免每次 make dev 都重装依赖。
build-editor-incremental: build-editor-incremental:
@cd libs/tiptap-editor && pnpm run build @cd libs/tiptap-editor && npm run build
build-lightbox: build-lightbox:
@echo "Building Lightbox..." @echo "Building Lightbox..."
@cd libs/lightbox && pnpm install && pnpm run build @cd libs/lightbox && npm install && npm run build
@echo "Lightbox built." @echo "Lightbox built."
# dev 用的增量构建:跳过 pnpm ci假设 node_modules 已存在),仅 vite build。 # dev 用的增量构建:跳过 npm ci假设 node_modules 已存在),仅 vite build。
build-lightbox-incremental: build-lightbox-incremental:
@cd libs/lightbox && pnpm run build @cd libs/lightbox && npm run build
build-core: dev: build-editor-incremental build-lightbox-incremental
@echo "Building yggdrasil-core..."
@cd libs/yggdrasil-core && pnpm install && pnpm run build
@echo "yggdrasil-core built."
# dev 用的增量构建:跳过 pnpm install假设 node_modules 已存在),仅 vite build。
build-core-incremental:
@cd libs/yggdrasil-core && pnpm run build
dev: build-editor-incremental build-lightbox-incremental build-core-incremental
@echo "Cleaning static/..." @echo "Cleaning static/..."
@rm -rf static/ @rm -rf static/
@echo "Building Tiptap editor (incremental)..." @echo "Building Tiptap editor (incremental)..."
@ -71,30 +55,16 @@ css-watch:
test: test:
@cargo test @cargo test
@cd libs/tiptap-editor && pnpm test @cd libs/tiptap-editor && npm test
@cd libs/lightbox && pnpm test @cd libs/lightbox && npm test
@cd libs/yggdrasil-core && pnpm test
# 只编译当前 crate 的文档(--no-deps 跳过依赖,--document-private-items # 只编译当前 crate 的文档(--no-deps 跳过依赖,--document-private-items
# 让纯 binary crate 的内部模块/私有项也进文档,否则页面基本是空的)。 # 让纯 binary crate 的内部模块/私有项也进文档,否则页面基本是空的)。
# RUSTDOCFLAGS 把 rustdoc 的 --default-theme=ayu 透传过去——cargo doc 本身 # RUSTDOCFLAGS 把 rustdoc 的 --default-theme=ayu 透传过去——cargo doc 本身
# 无主题参数,但会把该环境变量转交给底层 rustdoc。注意它是默认值浏览器 # 无主题参数,但会把该环境变量转交给底层 rustdoc。注意它是默认值浏览器
# 若已记住上次的主题选择localStorage则不会被覆盖。 # 若已记住上次的主题选择localStorage则不会被覆盖。
#
# 生成后拷贝到 public/doc/,让文档随 Dioxus 静态目录发布。先清空旧目录再
# 整体拷贝避免删除模块后残留旧文件。rustdoc 内部用相对路径引用资源
# (如 ../../static.files/),原样挂载不会断链。
#
# 额外生成 public/doc/index.html 重定向页Dioxus 在 dev 用
# nest_service("/doc", ServeDir) 托管该目录ServeDir 访问目录根时默认
# 返回 index.html。用 meta refresh + JS 跳转到真正的文档入口
# yggdrasil/index.html这样裸路径 /doc 也能直达文档,且不与 Dioxus 的
# /doc/* 路由冲突(手动注册 /doc 会在 merge 时 panic
doc: doc:
@RUSTDOCFLAGS="--default-theme=ayu" cargo doc --no-deps --document-private-items @RUSTDOCFLAGS="--default-theme=ayu" cargo doc --no-deps --document-private-items
@rm -rf public/doc
@cp -r target/doc public/doc
@printf '<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=yggdrasil/index.html"><title>Redirecting…</title></head><body><script>location.replace("yggdrasil/index.html")</script></body></html>' > public/doc/index.html
# 同 doc生成完自动用浏览器打开。 # 同 doc生成完自动用浏览器打开。
doc-open: doc-open:
@ -103,5 +73,4 @@ doc-open:
clean: clean:
@cargo clean @cargo clean
@rm -f public/style.css public/highlight.css @rm -f public/style.css public/highlight.css
@rm -rf public/doc
@rm -rf uploads/.cache @rm -rf uploads/.cache

View File

@ -29,13 +29,13 @@
@layer base { @layer base {
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
background-color: var(--color-paper-theme);
} }
body { body {
background-color: var(--color-paper-theme); background-color: var(--color-paper-theme);
color: var(--color-paper-primary); color: var(--color-paper-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
transition: background-color 0.3s ease, color 0.3s ease;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

1372
libs/lightbox/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,849 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
happy-dom:
specifier: ^20.10.6
version: 20.10.6
typescript:
specifier: ^6.0.3
version: 6.0.3
vite:
specifier: ^8.1.0
version: 8.1.0(@types/node@26.0.1)
vitest:
specifier: ^4.1.9
version: 4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1))
packages:
'@emnapi/core@1.11.1':
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
'@emnapi/runtime@1.11.1':
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@napi-rs/wasm-runtime@1.1.6':
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@oxc-project/types@0.137.0':
resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
'@rolldown/binding-android-arm64@1.1.3':
resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.1.3':
resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.1.3':
resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.1.3':
resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.1.3':
resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.1.3':
resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.1.3':
resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.1.3':
resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.1.3':
resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.1.3':
resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.1.3':
resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.1.3':
resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.1.3':
resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/node@26.0.1':
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@vitest/expect@4.1.9':
resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
'@vitest/mocker@4.1.9':
resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.1.9':
resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
'@vitest/runner@4.1.9':
resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
'@vitest/snapshot@4.1.9':
resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
'@vitest/spy@4.1.9':
resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
'@vitest/utils@4.1.9':
resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
buffer-image-size@0.6.4:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.4.0:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
happy-dom@20.10.6:
resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==}
engines: {node: '>=20.0.0'}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
rolldown@1.1.3:
resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.2.4:
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
engines: {node: '>=18'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
vite@8.1.0:
resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
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
vitest@4.1.9:
resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
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
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
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
snapshots:
'@emnapi/core@1.11.1':
dependencies:
'@emnapi/wasi-threads': 1.2.2
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.11.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.2':
dependencies:
tslib: 2.8.1
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
'@tybys/wasm-util': 0.10.3
optional: true
'@oxc-project/types@0.137.0': {}
'@rolldown/binding-android-arm64@1.1.3':
optional: true
'@rolldown/binding-darwin-arm64@1.1.3':
optional: true
'@rolldown/binding-darwin-x64@1.1.3':
optional: true
'@rolldown/binding-freebsd-x64@1.1.3':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-arm64-musl@1.1.3':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-x64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-x64-musl@1.1.3':
optional: true
'@rolldown/binding-openharmony-arm64@1.1.3':
optional: true
'@rolldown/binding-wasm32-wasi@1.1.3':
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.1.3':
optional: true
'@rolldown/binding-win32-x64-msvc@1.1.3':
optional: true
'@rolldown/pluginutils@1.0.1': {}
'@standard-schema/spec@1.1.0': {}
'@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
optional: true
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.9': {}
'@types/node@26.0.1':
dependencies:
undici-types: 8.3.0
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 26.0.1
'@vitest/expect@4.1.9':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.9
'@vitest/utils': 4.1.9
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1))':
dependencies:
'@vitest/spy': 4.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.1.0(@types/node@26.0.1)
'@vitest/pretty-format@4.1.9':
dependencies:
tinyrainbow: 3.1.0
'@vitest/runner@4.1.9':
dependencies:
'@vitest/utils': 4.1.9
pathe: 2.0.3
'@vitest/snapshot@4.1.9':
dependencies:
'@vitest/pretty-format': 4.1.9
'@vitest/utils': 4.1.9
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.9': {}
'@vitest/utils@4.1.9':
dependencies:
'@vitest/pretty-format': 4.1.9
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
assertion-error@2.0.1: {}
buffer-image-size@0.6.4:
dependencies:
'@types/node': 26.0.1
chai@6.2.2: {}
convert-source-map@2.0.0: {}
detect-libc@2.1.2: {}
entities@7.0.1: {}
es-module-lexer@2.1.0: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
expect-type@1.4.0: {}
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
happy-dom@20.10.6:
dependencies:
'@types/node': 26.0.1
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
buffer-image-size: 0.6.4
entities: 7.0.1
whatwg-mimetype: 3.0.0
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
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
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
nanoid@3.3.15: {}
obug@2.1.3: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.15:
dependencies:
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
rolldown@1.1.3:
dependencies:
'@oxc-project/types': 0.137.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm64': 1.1.3
'@rolldown/binding-darwin-arm64': 1.1.3
'@rolldown/binding-darwin-x64': 1.1.3
'@rolldown/binding-freebsd-x64': 1.1.3
'@rolldown/binding-linux-arm-gnueabihf': 1.1.3
'@rolldown/binding-linux-arm64-gnu': 1.1.3
'@rolldown/binding-linux-arm64-musl': 1.1.3
'@rolldown/binding-linux-ppc64-gnu': 1.1.3
'@rolldown/binding-linux-s390x-gnu': 1.1.3
'@rolldown/binding-linux-x64-gnu': 1.1.3
'@rolldown/binding-linux-x64-musl': 1.1.3
'@rolldown/binding-openharmony-arm64': 1.1.3
'@rolldown/binding-wasm32-wasi': 1.1.3
'@rolldown/binding-win32-arm64-msvc': 1.1.3
'@rolldown/binding-win32-x64-msvc': 1.1.3
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@4.1.0: {}
tinybench@2.9.0: {}
tinyexec@1.2.4: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tinyrainbow@3.1.0: {}
tslib@2.8.1:
optional: true
typescript@6.0.3: {}
undici-types@8.3.0: {}
vite@8.1.0(@types/node@26.0.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.15
rolldown: 1.1.3
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.0.1
fsevents: 2.3.3
vitest@4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1)):
dependencies:
'@vitest/expect': 4.1.9
'@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1))
'@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.1.0
expect-type: 1.4.0
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.1.0(@types/node@26.0.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 26.0.1
happy-dom: 20.10.6
transitivePeerDependencies:
- msw
whatwg-mimetype@3.0.0: {}
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
ws@8.21.0: {}

2093
libs/tiptap-editor/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
node_modules

View File

@ -1,19 +0,0 @@
{
"name": "@yggdrasil/core",
"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": {
"happy-dom": "^20.10.6",
"typescript": "^6.0.3",
"vite": "^8.1.0",
"vitest": "^4.1.9"
}
}

View File

@ -1,849 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
happy-dom:
specifier: ^20.10.6
version: 20.10.6
typescript:
specifier: ^6.0.3
version: 6.0.3
vite:
specifier: ^8.1.0
version: 8.1.0(@types/node@26.0.1)
vitest:
specifier: ^4.1.9
version: 4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1))
packages:
'@emnapi/core@1.11.1':
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
'@emnapi/runtime@1.11.1':
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@napi-rs/wasm-runtime@1.1.6':
resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@oxc-project/types@0.137.0':
resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
'@rolldown/binding-android-arm64@1.1.3':
resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.1.3':
resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.1.3':
resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.1.3':
resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.1.3':
resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.1.3':
resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.1.3':
resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.1.3':
resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.1.3':
resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.1.3':
resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.1.3':
resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.1.3':
resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.1.3':
resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/node@26.0.1':
resolution: {integrity: sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==}
'@types/whatwg-mimetype@3.0.2':
resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@vitest/expect@4.1.9':
resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==}
'@vitest/mocker@4.1.9':
resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.1.9':
resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==}
'@vitest/runner@4.1.9':
resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==}
'@vitest/snapshot@4.1.9':
resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==}
'@vitest/spy@4.1.9':
resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==}
'@vitest/utils@4.1.9':
resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
buffer-image-size@0.6.4:
resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==}
engines: {node: '>=4.0'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
es-module-lexer@2.1.0:
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.4.0:
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
happy-dom@20.10.6:
resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==}
engines: {node: '>=20.0.0'}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
obug@2.1.3:
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
engines: {node: '>=12.20.0'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss@8.5.15:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
rolldown@1.1.3:
resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.2.4:
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
engines: {node: '>=18'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@6.0.3:
resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
vite@8.1.0:
resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
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
vitest@4.1.9:
resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
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
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
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
snapshots:
'@emnapi/core@1.11.1':
dependencies:
'@emnapi/wasi-threads': 1.2.2
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.11.1':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.2':
dependencies:
tslib: 2.8.1
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
'@tybys/wasm-util': 0.10.3
optional: true
'@oxc-project/types@0.137.0': {}
'@rolldown/binding-android-arm64@1.1.3':
optional: true
'@rolldown/binding-darwin-arm64@1.1.3':
optional: true
'@rolldown/binding-darwin-x64@1.1.3':
optional: true
'@rolldown/binding-freebsd-x64@1.1.3':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.1.3':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-arm64-musl@1.1.3':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-x64-gnu@1.1.3':
optional: true
'@rolldown/binding-linux-x64-musl@1.1.3':
optional: true
'@rolldown/binding-openharmony-arm64@1.1.3':
optional: true
'@rolldown/binding-wasm32-wasi@1.1.3':
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
'@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.1.3':
optional: true
'@rolldown/binding-win32-x64-msvc@1.1.3':
optional: true
'@rolldown/pluginutils@1.0.1': {}
'@standard-schema/spec@1.1.0': {}
'@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
optional: true
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.9': {}
'@types/node@26.0.1':
dependencies:
undici-types: 8.3.0
'@types/whatwg-mimetype@3.0.2': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 26.0.1
'@vitest/expect@4.1.9':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.9
'@vitest/utils': 4.1.9
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1))':
dependencies:
'@vitest/spy': 4.1.9
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.1.0(@types/node@26.0.1)
'@vitest/pretty-format@4.1.9':
dependencies:
tinyrainbow: 3.1.0
'@vitest/runner@4.1.9':
dependencies:
'@vitest/utils': 4.1.9
pathe: 2.0.3
'@vitest/snapshot@4.1.9':
dependencies:
'@vitest/pretty-format': 4.1.9
'@vitest/utils': 4.1.9
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.9': {}
'@vitest/utils@4.1.9':
dependencies:
'@vitest/pretty-format': 4.1.9
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
assertion-error@2.0.1: {}
buffer-image-size@0.6.4:
dependencies:
'@types/node': 26.0.1
chai@6.2.2: {}
convert-source-map@2.0.0: {}
detect-libc@2.1.2: {}
entities@7.0.1: {}
es-module-lexer@2.1.0: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
expect-type@1.4.0: {}
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fsevents@2.3.3:
optional: true
happy-dom@20.10.6:
dependencies:
'@types/node': 26.0.1
'@types/whatwg-mimetype': 3.0.2
'@types/ws': 8.18.1
buffer-image-size: 0.6.4
entities: 7.0.1
whatwg-mimetype: 3.0.0
ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
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
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
nanoid@3.3.15: {}
obug@2.1.3: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@4.0.4: {}
postcss@8.5.15:
dependencies:
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
rolldown@1.1.3:
dependencies:
'@oxc-project/types': 0.137.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm64': 1.1.3
'@rolldown/binding-darwin-arm64': 1.1.3
'@rolldown/binding-darwin-x64': 1.1.3
'@rolldown/binding-freebsd-x64': 1.1.3
'@rolldown/binding-linux-arm-gnueabihf': 1.1.3
'@rolldown/binding-linux-arm64-gnu': 1.1.3
'@rolldown/binding-linux-arm64-musl': 1.1.3
'@rolldown/binding-linux-ppc64-gnu': 1.1.3
'@rolldown/binding-linux-s390x-gnu': 1.1.3
'@rolldown/binding-linux-x64-gnu': 1.1.3
'@rolldown/binding-linux-x64-musl': 1.1.3
'@rolldown/binding-openharmony-arm64': 1.1.3
'@rolldown/binding-wasm32-wasi': 1.1.3
'@rolldown/binding-win32-arm64-msvc': 1.1.3
'@rolldown/binding-win32-x64-msvc': 1.1.3
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@4.1.0: {}
tinybench@2.9.0: {}
tinyexec@1.2.4: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tinyrainbow@3.1.0: {}
tslib@2.8.1:
optional: true
typescript@6.0.3: {}
undici-types@8.3.0: {}
vite@8.1.0(@types/node@26.0.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.15
rolldown: 1.1.3
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.0.1
fsevents: 2.3.3
vitest@4.1.9(@types/node@26.0.1)(happy-dom@20.10.6)(vite@8.1.0(@types/node@26.0.1)):
dependencies:
'@vitest/expect': 4.1.9
'@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1))
'@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.1.0
expect-type: 1.4.0
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
picomatch: 4.0.4
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
vite: 8.1.0(@types/node@26.0.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 26.0.1
happy-dom: 20.10.6
transitivePeerDependencies:
- msw
whatwg-mimetype@3.0.0: {}
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
ws@8.21.0: {}

View File

@ -1,15 +0,0 @@
import { initPostContent } from './post-content';
import { startThemeTransition } from './theme-transition';
import './style.css';
declare global {
interface Window {
__initPostContent: (selector: string) => void;
__startThemeTransition: (x: number, y: number) => void;
}
}
window.__initPostContent = initPostContent;
window.__startThemeTransition = startThemeTransition;
export {};

View File

@ -1,73 +0,0 @@
/**
* post-content 测试:钉住代码块 copy
* 黑盒驱动:只通过 window.__initPostContent
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import './index';
describe('initPostContent', () => {
beforeEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
document.body.innerHTML = '';
});
it('为 pre>code 注入 .copy-code 按钮', () => {
const root = document.createElement('div');
root.className = 'post-content';
root.innerHTML = '<pre><code>console.log(1)</code></pre>';
document.body.appendChild(root);
window.__initPostContent('.post-content');
const btn = root.querySelector('pre .copy-code');
expect(btn).not.toBeNull();
expect(btn?.textContent).toBe('copy');
});
it('已有 .copy-code 的 pre 不重复注入', () => {
const root = document.createElement('div');
root.className = 'post-content';
root.innerHTML =
'<pre><code>x</code><button class="copy-code">copy</button></pre>';
document.body.appendChild(root);
window.__initPostContent('.post-content');
const btns = root.querySelectorAll('pre .copy-code');
expect(btns.length).toBe(1);
});
it('点击按钮调用 clipboard.writeText 并回显 copied!', () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
value: { writeText },
configurable: true,
});
const root = document.createElement('div');
root.className = 'post-content';
root.innerHTML = '<pre><code>hello</code></pre>';
document.body.appendChild(root);
window.__initPostContent('.post-content');
const btn = root.querySelector('.copy-code') as HTMLButtonElement;
btn.click();
expect(writeText).toHaveBeenCalledWith('hello');
expect(btn.textContent).toBe('copied!');
// 2 秒后还原回 'copy'
vi.advanceTimersByTime(2000);
expect(btn.textContent).toBe('copy');
});
it('selector 未命中时不报错', () => {
expect(() => window.__initPostContent('.not-exist')).not.toThrow();
});
});

View File

@ -1,48 +0,0 @@
/**
* copy
*
* pre>code .copy-code , copied!
* 2 input.css .copy-code ( Tailwind )
*/
function initCopyButtons(root: Element): void {
const blocks = root.querySelectorAll('pre > code');
blocks.forEach((code) => {
const pre = code.parentElement;
if (!pre) return;
if (pre.querySelector('.copy-code')) return;
const btn = document.createElement('button');
btn.className = 'copy-code';
btn.textContent = 'copy';
btn.setAttribute('aria-label', 'Copy code');
const codeText = code.textContent || '';
btn.addEventListener('click', () => {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(codeText);
} else {
const ta = document.createElement('textarea');
ta.value = codeText;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
btn.textContent = 'copied!';
setTimeout(() => {
btn.textContent = 'copy';
}, 2000);
});
pre.appendChild(btn);
});
}
export function initPostContent(selector: string): void {
const root = document.querySelector(selector);
if (!root) return;
initCopyButtons(root);
}

View File

@ -1,36 +0,0 @@
/* ========== 主题切换圆形展开(View Transitions API) ========== */
/* 取消所有 UA 默认的动画和颜色混合 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
/* 无论什么方向,始终让 new (新状态) 在上方,并从小圆扩展覆盖 old */
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 2;
animation: tt-expand 0.4s ease-out forwards;
/* 显式给伪元素加上不透明背景,防止截图透明导致看不到覆盖效果 */
background: var(--color-paper-theme);
}
/* CSS 变量由 JS 注入到 html style 中 */
@keyframes tt-expand {
from { clip-path: circle(0px at var(--tt-x) var(--tt-y)); }
to { clip-path: circle(var(--tt-r) at var(--tt-x) var(--tt-y)); }
}
/* View Transition 期间强制禁用所有 CSS 过渡,
确保 VT 回调中 toggle dark class 后截图捕获的是最终颜色 */
html.is-theme-transitioning,
html.is-theme-transitioning *,
html.is-theme-transitioning *::before,
html.is-theme-transitioning *::after {
transition: none !important;
}

View File

@ -1,108 +0,0 @@
/**
* theme-transition
*
* happy-dom document.startViewTransition,( dark class)
* mock startViewTransition 验证:调用它 CSS
*
* 注意:startThemeTransition (x, y),(/) DOM dark class
* (),
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import './index';
describe('startThemeTransition', () => {
beforeEach(() => {
document.documentElement.classList.remove('dark');
document.documentElement.classList.remove('is-theme-transitioning');
document.documentElement.style.cssText = '';
vi.restoreAllMocks();
});
afterEach(() => {
document.documentElement.classList.remove('dark');
document.documentElement.classList.remove('is-theme-transitioning');
document.documentElement.style.cssText = '';
});
it('降级:无 startViewTransition 时,亮→暗(无 dark class 时 add)', () => {
expect(document.documentElement.classList.contains('dark')).toBe(false);
window.__startThemeTransition(100, 200);
expect(document.documentElement.classList.contains('dark')).toBe(true);
});
it('降级:无 startViewTransition 时,暗→亮(有 dark class 时 remove)', () => {
document.documentElement.classList.add('dark');
window.__startThemeTransition(100, 200);
expect(document.documentElement.classList.contains('dark')).toBe(false);
});
it('主路径:有 startViewTransition 时调用它,注入变量,callback 切换 dark class', async () => {
const cbRef: { cb: (() => void) | null } = { cb: null };
const readyP = Promise.resolve();
const finishedP = Promise.resolve();
const startVT = vi.fn((cb: () => void) => {
cbRef.cb = cb;
return { ready: readyP, finished: finishedP, skipTransition: () => {} };
});
Object.defineProperty(document, 'startViewTransition', {
value: startVT,
configurable: true,
writable: true,
});
window.__startThemeTransition(100, 200);
expect(startVT).toHaveBeenCalledTimes(1);
// CSS 变量应注入
expect(document.documentElement.style.getPropertyValue('--tt-x')).toBe('100px');
expect(document.documentElement.style.getPropertyValue('--tt-y')).toBe('200px');
expect(document.documentElement.style.getPropertyValue('--tt-r')).toMatch(/^\d+(\.\d+)?px$/);
// is-theme-transitioning 应在 VT 之前添加
expect(document.documentElement.classList.contains('is-theme-transitioning')).toBe(true);
// callback 里根据 DOM 现状(无 dark)切到 dark
cbRef.cb!();
expect(document.documentElement.classList.contains('dark')).toBe(true);
// finished 后移除 is-theme-transitioning 和 CSS 变量
await finishedP;
await Promise.resolve();
// happy-dom microtask 可能需要额外 tick
await new Promise((r) => setTimeout(r, 0));
expect(document.documentElement.classList.contains('is-theme-transitioning')).toBe(false);
expect(document.documentElement.style.getPropertyValue('--tt-x')).toBe('');
delete (document as unknown as { startViewTransition?: unknown }).startViewTransition;
});
it('reduced-motion:即使有 startViewTransition 也走降级(瞬切)', () => {
const startVT = vi.fn(() => ({ ready: Promise.resolve(), finished: Promise.resolve(), skipTransition: () => {} }));
Object.defineProperty(document, 'startViewTransition', {
value: startVT,
configurable: true,
writable: true,
});
vi.stubGlobal('matchMedia', vi.fn((q: string) => ({
matches: q.includes('reduce'),
media: q,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false,
})));
window.__startThemeTransition(0, 0);
expect(startVT).not.toHaveBeenCalled();
expect(document.documentElement.classList.contains('dark')).toBe(true);
delete (document as unknown as { startViewTransition?: unknown }).startViewTransition;
});
});

View File

@ -1,84 +0,0 @@
/**
* (View Transitions API)
*
* CSS CSS
* 核心策略:始终让"暗色层", clip-path "亮色层"
* - -> : NEW (),(`tt-expand`) OLD
* - -> : OLD (),(`tt-shrink`) NEW
*
* WAAPI <style>,DOM
* API bug, VT
*/
function prefersReducedMotion(): boolean {
return (
!!window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
}
function maxCornerDistance(x: number, y: number): number {
const w = window.innerWidth;
const h = window.innerHeight;
const corners = [
[0, 0],
[w, 0],
[0, h],
[w, h],
];
let max = 0;
for (const [cx, cy] of corners) {
const d = Math.hypot(cx - x, cy - y);
if (d > max) max = d;
}
return max;
}
function applyDarkClass(isDark: boolean): void {
const html = document.documentElement;
if (isDark) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
}
export function startThemeTransition(x: number, y: number): void {
const html = document.documentElement;
const isDark = !html.classList.contains('dark');
const hasVT = typeof document.startViewTransition === 'function';
const reduced = prefersReducedMotion();
if (!hasVT || reduced) {
applyDarkClass(isDark);
return;
}
const maxR = maxCornerDistance(x, y);
// 注入动画需要的 CSS 变量
html.style.setProperty('--tt-x', `${x}px`);
html.style.setProperty('--tt-y', `${y}px`);
html.style.setProperty('--tt-r', `${maxR}px`);
// 禁用所有 CSS transition,确保 VT 截图是最终颜色
html.classList.add('is-theme-transitioning');
const vt = document.startViewTransition(() => {
applyDarkClass(isDark);
// 强制同步样式重算:确保 body 的 background-color 解析为目标值
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
getComputedStyle(document.body).backgroundColor;
});
vt.ready.catch(() => {});
vt.finished.finally(() => {
html.classList.remove('is-theme-transitioning');
// 清理 CSS 变量
html.style.removeProperty('--tt-x');
html.style.removeProperty('--tt-y');
html.style.removeProperty('--tt-r');
});
}

View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,19 +0,0 @@
{
"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"]
}

View File

@ -1,23 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
outDir: resolve(__dirname, '../../public/yggdrasil-core'),
emptyOutDir: true,
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'YggdrasilCore',
fileName: () => 'yggdrasil-core.js',
formats: ['iife'],
},
rolldownOptions: {
output: {
assetFileNames: 'yggdrasil-core.[ext]',
},
},
cssCodeSplit: false,
minify: true,
sourcemap: true,
},
});

View File

@ -1,8 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
include: ['src/**/*.test.ts'],
},
});

48
public/js/post-content.js Normal file
View File

@ -0,0 +1,48 @@
(function () {
"use strict";
function initCopyButtons(root) {
var blocks = root.querySelectorAll("pre > code");
for (var i = 0; i < blocks.length; i++) {
var code = blocks[i];
var pre = code.parentElement;
if (!pre) continue;
if (pre.querySelector(".copy-code")) continue;
var btn = document.createElement("button");
btn.className = "copy-code";
btn.textContent = "copy";
btn.setAttribute("aria-label", "Copy code");
(function (codeText) {
btn.addEventListener("click", function () {
var self = this;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(codeText);
} else {
var ta = document.createElement("textarea");
ta.value = codeText;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
self.textContent = "copied!";
setTimeout(function () {
self.textContent = "copy";
}, 2000);
});
})(code.textContent || "");
pre.appendChild(btn);
}
}
window.__initPostContent = function (selector) {
var root = document.querySelector(selector);
if (!root) return;
initCopyButtons(root);
};
})();

View File

@ -14,8 +14,7 @@ use dioxus::router::components::Link;
#[derive(Props, Clone, PartialEq)] #[derive(Props, Clone, PartialEq)]
pub struct EmptyStateAction { pub struct EmptyStateAction {
/// 按钮文案。 /// 按钮文案。
#[props(into)] pub label: &'static str,
pub label: String,
/// 跳转目标路由。 /// 跳转目标路由。
pub to: crate::router::Route, pub to: crate::router::Route,
} }
@ -29,14 +28,11 @@ pub struct EmptyStateAction {
#[component] #[component]
pub fn EmptyState( pub fn EmptyState(
/// 主标题(通常为「还没有文章」之类)。 /// 主标题(通常为「还没有文章」之类)。
#[props(into, default = "还没有文章".to_string())] #[props(default)] title: Option<&'static str>,
title: String,
/// 副文案,说明当前状态或引导用户。 /// 副文案,说明当前状态或引导用户。
#[props(into, default = String::new())] #[props(default)] description: Option<&'static str>,
description: String,
/// 可选的行动按钮。 /// 可选的行动按钮。
#[props(default)] #[props(default)] action: Option<EmptyStateAction>,
action: Option<EmptyStateAction>,
) -> Element { ) -> Element {
rsx! { rsx! {
div { class: "flex flex-col items-center justify-center text-center py-20 px-4 page-enter", div { class: "flex flex-col items-center justify-center text-center py-20 px-4 page-enter",
@ -49,12 +45,12 @@ pub fn EmptyState(
} }
// 主标题:衬线字体,与首页 H1 风格呼应但更轻量。 // 主标题:衬线字体,与首页 H1 风格呼应但更轻量。
h2 { class: "mt-8 text-2xl font-bold tracking-tight text-paper-primary", h2 { class: "mt-8 text-2xl font-bold tracking-tight text-paper-primary",
"{title}" {title.unwrap_or("还没有文章")}
} }
// 副文案:次要色,限宽保证可读性。 // 副文案:次要色,限宽保证可读性。
if !description.is_empty() { if let Some(desc) = description {
p { class: "mt-3 text-sm leading-relaxed text-paper-secondary max-w-md", p { class: "mt-3 text-sm leading-relaxed text-paper-secondary max-w-md",
"{description}" {desc}
} }
} }
// 行动按钮:药丸形,与搜索页主按钮一致。 // 行动按钮:药丸形,与搜索页主按钮一致。
@ -62,7 +58,7 @@ pub fn EmptyState(
Link { Link {
class: "mt-8 inline-flex items-center px-6 py-2 bg-paper-accent text-white rounded-full font-medium text-sm hover:brightness-110 active:scale-[0.98] transition-all duration-200", class: "mt-8 inline-flex items-center px-6 py-2 bg-paper-accent text-white rounded-full font-medium text-sm hover:brightness-110 active:scale-[0.98] transition-all duration-200",
to: act.to, to: act.to,
"{act.label}" {act.label}
} }
} }
} }

View File

@ -10,14 +10,14 @@ use dioxus::prelude::*;
/// - `content_html`:服务端渲染的文章 HTML 字符串 /// - `content_html`:服务端渲染的文章 HTML 字符串
/// ///
/// 关键行为: /// 关键行为:
/// - 在 `target_arch = "wasm32"` 环境下调用 `window.__initPostContent` 初始化代码块 /// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。
/// 复制按钮(`yggdrasil-core.js` 已由 `Dioxus.toml` 全局注入)。
/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js` /// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`
/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。 /// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
#[component] #[component]
pub fn PostContent(content_html: String) -> Element { pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use_effect(move || { use_effect(move || {
let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
let _ = js_sys::eval("window.__initPostContent('.post-content')"); let _ = js_sys::eval("window.__initPostContent('.post-content')");
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。 // lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用; // 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;

View File

@ -391,6 +391,23 @@ fn main() {
"/readyz", "/readyz",
axum::routing::get(crate::api::health::readyz), axum::routing::get(crate::api::health::readyz),
) )
// /doc 与 /doc/ 重定向到文档入口 /doc/yggdrasil/index.html。
// public/doc/ 由 Dioxus 自动托管,深层路径无需额外处理;仅裸路径需要
// 重定向,否则会落到 Router 的 /:..segments 兜底返回 404 页面。
// 放在 static_routes与 /uploads 同层),不经过 CSRF/超时/缓存中间件,
// 行为与静态资源一致。用 301 永久重定向,让浏览器/书签记住跳转。
.route(
"/doc",
axum::routing::get(|| async {
axum::response::Redirect::permanent("/doc/yggdrasil/index.html")
}),
)
.route(
"/doc/",
axum::routing::get(|| async {
axum::response::Redirect::permanent("/doc/yggdrasil/index.html")
}),
)
.route( .route(
"/uploads/{*path}", "/uploads/{*path}",
axum::routing::get(crate::api::image::serve_image), axum::routing::get(crate::api::image::serve_image),

View File

@ -11,7 +11,6 @@ use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
use crate::api::posts::{list_published_posts, PostListResponse}; use crate::api::posts::{list_published_posts, PostListResponse};
use crate::components::empty_state::EmptyState;
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton; use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
use crate::models::post::PostListItem; use crate::models::post::PostListItem;
@ -120,26 +119,17 @@ fn ArchivesContent() -> Element {
let posts_data = posts_res.read(); let posts_data = posts_res.read();
match &*posts_data { match &*posts_data {
Some(Ok(PostListResponse { posts, total })) => { Some(Ok(PostListResponse { posts, total })) => {
if *total == 0 { let grouped = group_posts(posts);
rsx! { rsx! {
EmptyState { div { class: "mt-2 text-base text-paper-secondary",
title: "还没有文章归档", ""
description: "发布文章后,这里会自动按年月进行归档显示。", span { class: "font-medium text-paper-primary", "{total}" }
} " 篇文章"
} }
} else { for year_group in grouped.iter() {
let grouped = group_posts(posts); YearSection {
rsx! { key: "{year_group.year}",
div { class: "mt-2 text-base text-paper-secondary", year_group: year_group.clone(),
""
span { class: "font-medium text-paper-primary", "{total}" }
" 篇文章"
}
for year_group in grouped.iter() {
YearSection {
key: "{year_group.year}",
year_group: year_group.clone(),
}
} }
} }
} }

View File

@ -22,7 +22,7 @@ use crate::pages::post_detail::PostDetail;
use crate::pages::register::Register; use crate::pages::register::Register;
use crate::pages::search::Search; use crate::pages::search::Search;
use crate::pages::tags::{TagDetail, Tags}; use crate::pages::tags::{TagDetail, Tags};
use crate::theme::{use_theme_provider, ThemePreload}; use crate::theme::{use_theme_provider, Theme, ThemePreload};
/// 全站路由枚举,每个变体对应一个页面路径 /// 全站路由枚举,每个变体对应一个页面路径
#[derive(Clone, Routable, Debug, PartialEq)] #[derive(Clone, Routable, Debug, PartialEq)]
@ -106,7 +106,12 @@ pub enum Route {
/// 初始化主题提供者、全局用户上下文,并挂载样式表与 `Router`。 /// 初始化主题提供者、全局用户上下文,并挂载样式表与 `Router`。
#[component] #[component]
pub fn AppRouter() -> Element { pub fn AppRouter() -> Element {
let _theme = use_theme_provider(); // 获取当前主题以设置顶层 dark 类
let theme = use_theme_provider();
let theme_class = match theme() {
Theme::Dark => "dark",
Theme::Light => "",
};
// 提供全局用户上下文,供登录状态与路由守卫使用 // 提供全局用户上下文,供登录状态与路由守卫使用
let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>); let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
@ -117,7 +122,7 @@ pub fn AppRouter() -> Element {
document::Stylesheet { href: "/style.css" } document::Stylesheet { href: "/style.css" }
document::Stylesheet { href: "/highlight.css" } document::Stylesheet { href: "/highlight.css" }
document::Title { "Yggdrasil Blog" } document::Title { "Yggdrasil Blog" }
div { div { class: "{theme_class}",
ThemePreload {} ThemePreload {}
Router::<Route> {} Router::<Route> {}
} }

View File

@ -6,11 +6,6 @@
//! `prefers-color-scheme` 媒体查询;切换时同步更新 DOM class 与 localStorage。 //! `prefers-color-scheme` 媒体查询;切换时同步更新 DOM class 与 localStorage。
use dioxus::prelude::*; use dioxus::prelude::*;
// InteractionLocation 提供 client_coordinates(),用于读取鼠标点击的视口坐标。
// 该 trait 不在 dioxus::prelude(后者只 re-export events::*),需单独从 dioxus::html 引入。
// 仅 WASM 前端用到(ThemeToggle 的圆形展开动画取点击坐标),服务端构建剥离。
#[cfg(target_arch = "wasm32")]
use dioxus::html::InteractionLocation;
/// localStorage 中存储主题值的键名。 /// localStorage 中存储主题值的键名。
#[cfg(any(target_arch = "wasm32", test))] #[cfg(any(target_arch = "wasm32", test))]
@ -92,19 +87,28 @@ fn detect_initial_theme() -> Theme {
/// 提供主题上下文的 Hook。 /// 提供主题上下文的 Hook。
/// ///
/// 初始化时按 SSR Cookie → WASM localStorage → 系统偏好的顺序检测主题; /// 初始化时按 SSR Cookie → WASM localStorage → 系统偏好的顺序检测主题;
/// 主题变化时将值持久化到 localStorage。 /// 主题变化时同步更新 HTML 根元素的 `dark` class 与 localStorage。
///
/// `<html>` 的 `dark` class 不在此处管理。WASM 端由 `ThemeToggle` 的 onclick
/// 通过 `yggdrasil-core.js` 的圆形展开动画在 View Transition 回调里同步 toggle。
/// 初始 class 由 `ThemePreload` 首屏脚本设置,避免闪烁。
pub fn use_theme_provider() -> Signal<Theme> { pub fn use_theme_provider() -> Signal<Theme> {
let theme = use_signal(detect_initial_theme); let theme = use_signal(detect_initial_theme);
use_effect(move || { use_effect(move || {
let current = theme();
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
let current = theme();
if let Some(window) = web_sys::window() { if let Some(window) = web_sys::window() {
// 同步 HTML 根元素的 dark class用于 Tailwind dark mode。
if let Some(document) = window.document() {
if let Some(html) = document.document_element() {
match current {
Theme::Dark => {
let _ = html.class_list().add_1("dark");
}
Theme::Light => {
let _ = html.class_list().remove_1("dark");
}
}
}
}
// 将当前主题持久化到 localStorage。 // 将当前主题持久化到 localStorage。
if let Ok(Some(storage)) = window.local_storage() { if let Ok(Some(storage)) = window.local_storage() {
let theme_str = match current { let theme_str = match current {
@ -115,8 +119,6 @@ pub fn use_theme_provider() -> Signal<Theme> {
} }
} }
} }
// 避免 unused 警告:非 wasm 构建下 current 未被读取。
let _ = current;
}); });
use_context_provider(|| theme); use_context_provider(|| theme);
@ -153,55 +155,16 @@ pub fn ThemePreload() -> Element {
} }
/// 主题切换按钮组件。 /// 主题切换按钮组件。
// evt 仅在 wasm32 用于取点击坐标,服务端构建剥离,故允许非 wasm 的 unused_variables。
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))]
#[component] #[component]
pub fn ThemeToggle() -> Element { pub fn ThemeToggle() -> Element {
let mut theme = use_theme(); let mut theme = use_theme();
// generation:每次点击递增。延迟回调检查自己的 gen 是否最新,过期则跳过 set。
// 解决连续点击时多个 spawn_local 堆积导致的状态错乱。
#[allow(unused_mut)]
let mut click_gen = use_signal(|| 0u32);
rsx! { rsx! {
button { button {
class: "theme-toggle p-2 rounded-full cursor-pointer hover:text-paper-accent transition-colors duration-200 text-paper-secondary", class: "theme-toggle p-2 rounded-full cursor-pointer hover:text-paper-accent transition-colors duration-200 text-paper-secondary",
r#type: "button", r#type: "button",
aria_label: "切换深色/浅色主题", aria_label: "切换深色/浅色主题",
onclick: move |evt| { onclick: move |_| theme.set(theme().toggle()),
let next = theme().toggle();
#[cfg(target_arch = "wasm32")]
{
let coords = evt.client_coordinates();
let x = coords.x;
let y = coords.y;
// JS 从 DOM 现状推导目标主题(不传 isDark),避免与 Signal 状态不同步。
let _ = js_sys::eval(&format!(
"if (window.__startThemeTransition) \
window.__startThemeTransition({x}, {y});",
x = x,
y = y,
));
// theme.set 推迟到动画结束:其触发的 Dioxus 微任务重渲染会打断 VT。
// gen 确保连续点击只有最新回调 set。JS 已自治切换 dark class,
// 动画完成后再更新 Dioxus 状态,避免组件重渲染可能带来的干扰
let gen = click_gen() + 1;
click_gen.set(gen);
let mut theme_clone = theme;
let mut gen_clone = click_gen;
wasm_bindgen_futures::spawn_local(async move {
crate::utils::time::sleep_ms(450).await;
if gen_clone() == gen {
theme_clone.set(next);
}
});
return;
}
#[cfg(not(target_arch = "wasm32"))]
{
theme.set(next);
}
},
if theme() == Theme::Dark { if theme() == Theme::Dark {
svg { svg {
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",