diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/docs/.oxlintrc.json b/docs/.oxlintrc.json
new file mode 100644
index 0000000..6fa991d
--- /dev/null
+++ b/docs/.oxlintrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..355e0e2
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,35 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
+
+Note: This will impact Vite dev & build performances.
+You can also try [the experimental native React Compiler support in plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md#rust-react-compiler) by using `compiler: true` in the plugin options instead of using the Babel plugin.
+
+## Expanding the Oxlint configuration
+
+If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
+
+```json
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc"],
+ "options": {
+ "typeAware": true
+ },
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
+ }
+}
+```
+
+See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
diff --git a/docs/bun.lock b/docs/bun.lock
new file mode 100644
index 0000000..ed7f141
--- /dev/null
+++ b/docs/bun.lock
@@ -0,0 +1,255 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "docs",
+ "dependencies": {
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ },
+ "devDependencies": {
+ "@babel/core": "^7.29.7",
+ "@rolldown/plugin-babel": "^0.2.3",
+ "@types/babel__core": "^7.20.5",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.7",
+ "@vitejs/plugin-react": "^6.1.1",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxlint": "^1.81.0",
+ "typescript": "~6.0.2",
+ "vite": "^8.3.0",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
+
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
+
+ "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
+
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
+
+ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="],
+
+ "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.83.0", "", { "os": "android", "cpu": "arm" }, "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA=="],
+
+ "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.83.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg=="],
+
+ "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.83.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ=="],
+
+ "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.83.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA=="],
+
+ "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.83.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w=="],
+
+ "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw=="],
+
+ "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA=="],
+
+ "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg=="],
+
+ "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew=="],
+
+ "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.83.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g=="],
+
+ "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw=="],
+
+ "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw=="],
+
+ "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.83.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ=="],
+
+ "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw=="],
+
+ "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ=="],
+
+ "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.83.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA=="],
+
+ "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.83.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg=="],
+
+ "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.83.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA=="],
+
+ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.83.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q=="],
+
+ "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="],
+
+ "@rolldown/plugin-babel": ["@rolldown/plugin-babel@0.2.4", "", { "dependencies": { "picomatch": "^4.0.7" }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "optionalPeers": ["@babel/plugin-transform-runtime", "@babel/runtime", "vite"] }, "sha512-ygmm2j/mN+aCvstHo0x3IOu4uc6LqXT9SJ3q4kpfG/+Yhfz0B+E47FITcC0FljZADwUFARP6E2kiE+Lc26D8wg=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+ "@types/node": ["@types/node@24.13.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug=="],
+
+ "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
+
+ "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="],
+
+ "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg=="],
+
+ "browserslist": ["browserslist@4.29.0", "", { "dependencies": { "baseline-browser-mapping": "^2.11.23", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.427", "node-releases": "^2.0.55", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.430", "", {}, "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
+
+ "node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
+
+ "oxlint": ["oxlint@1.83.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.83.0", "@oxlint/binding-android-arm64": "1.83.0", "@oxlint/binding-darwin-arm64": "1.83.0", "@oxlint/binding-darwin-x64": "1.83.0", "@oxlint/binding-freebsd-x64": "1.83.0", "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", "@oxlint/binding-linux-arm-musleabihf": "1.83.0", "@oxlint/binding-linux-arm64-gnu": "1.83.0", "@oxlint/binding-linux-arm64-musl": "1.83.0", "@oxlint/binding-linux-ppc64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-musl": "1.83.0", "@oxlint/binding-linux-s390x-gnu": "1.83.0", "@oxlint/binding-linux-x64-gnu": "1.83.0", "@oxlint/binding-linux-x64-musl": "1.83.0", "@oxlint/binding-openharmony-arm64": "1.83.0", "@oxlint/binding-win32-arm64-msvc": "1.83.0", "@oxlint/binding-win32-ia32-msvc": "1.83.0", "@oxlint/binding-win32-x64-msvc": "1.83.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
+
+ "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
+
+ "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
+
+ "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
+
+ "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="],
+
+ "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
+
+ "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+ }
+}
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..dfbcf85
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ docs
+
+
+
+
+
+
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 0000000..c5257f9
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "docs",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "oxlint",
+ "preview": "vite preview",
+ "typecheck": "tsc -b"
+ },
+ "dependencies": {
+ "@icones/react": "^0.0.1",
+ "@workspace/i18n": "workspace:*",
+ "@workspace/preferences": "workspace:*",
+ "@workspace/ui": "workspace:*",
+ "@workspace/search": "workspace:*",
+ "clsx": "^2.1.1",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "tailwind-merge": "^3.7.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.29.7",
+ "@icones/vite": "^0.0.1",
+ "@rolldown/plugin-babel": "^0.2.3",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/babel__core": "^7.20.5",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.7",
+ "@vitejs/plugin-react": "^6.1.1",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxlint": "^1.81.0",
+ "satteri": "^0.10.5",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.3.0",
+ "vite-plugin-satteri": "^0.3.5"
+ }
+}
diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/docs/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/icons.svg b/docs/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/docs/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/docs/src/App.tsx b/docs/src/App.tsx
new file mode 100644
index 0000000..f7f35b2
--- /dev/null
+++ b/docs/src/App.tsx
@@ -0,0 +1,145 @@
+import {
+ Suspense,
+ lazy,
+ startTransition,
+ useEffect,
+ useState,
+ type ComponentType,
+} from "react"
+import { rootCSSVariables } from "./components/layout"
+import { LocaleProvider } from "./components/locale-provider"
+import { DocsPreferencesProvider } from "./components/preferences-provider"
+import { DocsSearchProvider } from "./components/docs-search-provider"
+import { resolveDocLocale } from "./content/registry"
+import { useTranslate } from "./lib/locale"
+
+const Index = lazy(() => import("./pages/index"))
+const Article = lazy(() => import("./pages/article"))
+const Settings = lazy(() => import("./pages/settings"))
+
+type RouteDefinition = {
+ Component: ComponentType
+ matches: (pathname: string) => boolean
+}
+
+const routes: RouteDefinition[] = [
+ {
+ Component: Index,
+ matches: (pathname) =>
+ pathname === "/" || /^\/(?:en-US|zh-Hans)\/?$/.test(pathname),
+ },
+ {
+ Component: Article,
+ matches: (pathname) =>
+ /^(?:\/(?:en-US|zh-Hans))?\/articles?(?:\/|$)/.test(pathname),
+ },
+ {
+ Component: Settings,
+ matches: (pathname) =>
+ /^(?:\/(?:en-US|zh-Hans))?\/settings\/?$/.test(pathname),
+ },
+]
+
+function App() {
+ const [currentUrl, setCurrentUrl] = useState(() => window.location.href)
+
+ useEffect(() => {
+ function updateRoute() {
+ startTransition(() => setCurrentUrl(window.location.href))
+ }
+
+ function navigate(event: MouseEvent) {
+ if (
+ event.defaultPrevented ||
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey
+ ) {
+ return
+ }
+
+ const target = event.target
+ if (!(target instanceof Element)) return
+
+ const anchor = target.closest("a[href]")
+ if (
+ !anchor ||
+ anchor.hasAttribute("download") ||
+ (anchor.target && anchor.target !== "_self")
+ ) {
+ return
+ }
+
+ const destination = new URL(anchor.href, window.location.href)
+ if (destination.origin !== window.location.origin) return
+
+ const current = new URL(window.location.href)
+ if (
+ destination.pathname === current.pathname &&
+ destination.search === current.search &&
+ destination.hash
+ ) {
+ return
+ }
+
+ event.preventDefault()
+ window.history.pushState(null, "", destination)
+ window.scrollTo({ left: 0, top: 0 })
+ updateRoute()
+ }
+
+ window.addEventListener("popstate", updateRoute)
+ document.addEventListener("click", navigate)
+
+ return () => {
+ window.removeEventListener("popstate", updateRoute)
+ document.removeEventListener("click", navigate)
+ }
+ }, [])
+
+ const { pathname } = new URL(currentUrl)
+ const Route = routes.find((route) => route.matches(pathname))?.Component
+ const locale = resolveDocLocale(pathname)
+
+ return (
+
+
+
+
+
+ Loading…
+
+ }
+ >
+ {Route ? : }
+
+
+
+
+
+ )
+}
+
+function NotFound() {
+ const t = useTranslate()
+
+ return (
+
+
+ {t({ zh: "页面不存在", us: "Page not found" })}
+
+
+ {t({ zh: "返回首页", us: "Back to home" })}
+
+
+ )
+}
+
+export default App
diff --git a/docs/src/assets/hero.png b/docs/src/assets/hero.png
new file mode 100644
index 0000000..02251f4
Binary files /dev/null and b/docs/src/assets/hero.png differ
diff --git a/docs/src/assets/react.svg b/docs/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/docs/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/src/assets/vite.svg b/docs/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/docs/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/docs/src/components/article.tsx b/docs/src/components/article.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/docs/src/components/docs-search-provider.tsx b/docs/src/components/docs-search-provider.tsx
new file mode 100644
index 0000000..1ccc3c8
--- /dev/null
+++ b/docs/src/components/docs-search-provider.tsx
@@ -0,0 +1,179 @@
+import { I18nProvider } from "@workspace/i18n"
+import {
+ SearchDialog,
+ SearchProvider,
+ type SearchAdapter,
+ type SearchResultItem,
+} from "@workspace/search"
+import { messages as searchMessagesEnUS } from "@workspace/search/locales/en-US"
+import { messages as searchMessagesZhHans } from "@workspace/search/locales/zh-Hans"
+import React from "react"
+
+import {
+ getDocHref,
+ getDocPackages,
+ getDocSearchText,
+ getDocSectionLabel,
+} from "../content/registry"
+import type {
+ DocLocale,
+ DocPackage,
+ DocPage,
+ DocTocItem,
+} from "../content/types"
+import { useLocale } from "../lib/locale"
+
+const searchCatalogs = {
+ "en-US": searchMessagesEnUS,
+ "zh-Hans": searchMessagesZhHans,
+} as const
+
+type DocsSearchPayload = {
+ href: string
+}
+
+export function DocsSearchProvider({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const locale = useLocale()
+ const adapter = React.useMemo(() => createDocsSearchAdapter(locale), [locale])
+
+ return (
+
+ {
+ if (isDocsSearchPayload(item.payload)) navigate(item.payload.href)
+ }}
+ >
+ {children}
+
+
+
+ )
+}
+
+function createDocsSearchAdapter(locale: DocLocale): SearchAdapter {
+ const docPackages = getDocPackages(locale)
+
+ return {
+ async search({ query, signal }) {
+ const normalizedQuery = normalize(query)
+ if (!normalizedQuery) return { groups: [] }
+
+ const terms = normalizedQuery.split(/\s+/).filter(Boolean)
+ const groups = docPackages.flatMap((docPackage) => {
+ const items = docPackage.pages
+ .map((page) => ({
+ item: createResultItem(docPackage, page, locale),
+ score: getMatchScore(
+ normalizedQuery,
+ terms,
+ docPackage.label,
+ page
+ ),
+ }))
+ .filter((result) => result.score >= 0)
+ .sort(
+ (left, right) =>
+ right.score - left.score ||
+ left.item.title.localeCompare(right.item.title, locale)
+ )
+ .map((result) => result.item)
+
+ return items.length > 0
+ ? [
+ {
+ id: docPackage.key,
+ label: docPackage.label,
+ items,
+ total: items.length,
+ },
+ ]
+ : []
+ })
+
+ if (signal.aborted) return { groups: [] }
+ return { groups }
+ },
+ }
+}
+
+function createResultItem(
+ docPackage: DocPackage,
+ page: DocPage,
+ locale: DocLocale
+): SearchResultItem {
+ return {
+ id: `${page.section}/${page.slug}`,
+ title: page.title,
+ description: page.description,
+ keywords: [
+ docPackage.label,
+ getDocSectionLabel(page.section, locale),
+ ...page.toc.flatMap(flattenTocTitles),
+ getDocSearchText(page),
+ ],
+ meta: [getDocSectionLabel(page.section, locale)],
+ payload: {
+ href: getDocHref(docPackage, page),
+ } satisfies DocsSearchPayload,
+ }
+}
+
+function getMatchScore(
+ query: string,
+ terms: readonly string[],
+ packageLabel: string,
+ page: DocPage
+) {
+ const title = normalize(page.title)
+ const description = normalize(page.description)
+ const packageName = normalize(packageLabel)
+ const toc = normalize(page.toc.flatMap(flattenTocTitles).join(" "))
+ const body = normalize(getDocSearchText(page))
+ const searchableText = [packageName, title, description, toc, body].join(" ")
+
+ if (!terms.every((term) => searchableText.includes(term))) return -1
+
+ let score = 0
+ if (title === query) score += 100
+ else if (title.startsWith(query)) score += 70
+ else if (title.includes(query)) score += 50
+ if (packageName === query) score += 40
+ else if (packageName.includes(query)) score += 20
+ if (description.includes(query)) score += 15
+ if (toc.includes(query)) score += 10
+ if (page.section === "overview") score += 2
+
+ return score
+}
+
+function flattenTocTitles(item: DocTocItem): readonly string[] {
+ return [item.title, ...(item.items?.flatMap(flattenTocTitles) ?? [])]
+}
+
+function normalize(value: string) {
+ return value.normalize("NFKC").toLocaleLowerCase().trim()
+}
+
+function isDocsSearchPayload(value: unknown): value is DocsSearchPayload {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ "href" in value &&
+ typeof value.href === "string"
+ )
+}
+
+function navigate(href: string) {
+ const destination = new URL(href, window.location.href)
+ if (destination.href === window.location.href) return
+
+ window.history.pushState(null, "", destination)
+ window.scrollTo({ left: 0, top: 0 })
+ window.dispatchEvent(new PopStateEvent("popstate"))
+}
diff --git a/docs/src/components/language-switcher.tsx b/docs/src/components/language-switcher.tsx
new file mode 100644
index 0000000..d16ebed
--- /dev/null
+++ b/docs/src/components/language-switcher.tsx
@@ -0,0 +1,264 @@
+import { Icon } from "@icones/react"
+import { usePreference } from "@workspace/preferences"
+import { useEffect, useId, useRef, useState } from "react"
+import { createPortal } from "react-dom"
+
+import { LocaleFlag } from "./locale-flag"
+import { getLocalizedHref } from "../content/registry"
+import type { DocLocale } from "../content/types"
+import { useLocale, useTranslate } from "../lib/locale"
+import { useIsMobile } from "../lib/use-mobile"
+
+type LocaleOption = {
+ label: string
+ locale: DocLocale
+}
+
+const localeOptions: readonly LocaleOption[] = [
+ { label: "简体中文", locale: "zh-Hans" },
+ { label: "English", locale: "en-US" },
+]
+
+export function LanguageSwitcher() {
+ const locale = useLocale()
+ const t = useTranslate()
+ const isMobile = useIsMobile()
+ const [, setLocaleMode] = usePreference("locale-mode")
+ const [, setPreferredLocale] = usePreference("locale")
+ const [open, setOpen] = useState(false)
+ const rootRef = useRef(null)
+ const menuRef = useRef(null)
+ const triggerRef = useRef(null)
+ const menuId = useId()
+ const selectedLocale = getLocaleOption(locale)
+
+ useEffect(() => {
+ if (!open) return
+
+ function isInsideSwitcher(target: EventTarget | null) {
+ return (
+ target instanceof Node &&
+ (rootRef.current?.contains(target) || menuRef.current?.contains(target))
+ )
+ }
+
+ function closeAndRestoreFocus() {
+ setOpen(false)
+ window.requestAnimationFrame(() => triggerRef.current?.focus())
+ }
+
+ function handlePointerDown(event: PointerEvent) {
+ if (isInsideSwitcher(event.target)) return
+
+ if (isMobile) {
+ closeAndRestoreFocus()
+ } else {
+ setOpen(false)
+ }
+ }
+
+ function handleFocusIn(event: FocusEvent) {
+ if (!isMobile && !isInsideSwitcher(event.target)) setOpen(false)
+ }
+
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ event.preventDefault()
+ closeAndRestoreFocus()
+ return
+ }
+
+ if (!isMobile || event.key !== "Tab") return
+
+ const items = getMenuItems(menuRef.current)
+ const firstItem = items[0]
+ const lastItem = items.at(-1)
+ if (!firstItem || !lastItem) return
+
+ if (event.shiftKey && document.activeElement === firstItem) {
+ event.preventDefault()
+ lastItem.focus()
+ } else if (!event.shiftKey && document.activeElement === lastItem) {
+ event.preventDefault()
+ firstItem.focus()
+ }
+ }
+
+ const previousOverflow = document.body.style.overflow
+ if (isMobile) {
+ document.body.style.overflow = "hidden"
+ window.requestAnimationFrame(() => {
+ menuRef.current
+ ?.querySelector('[aria-checked="true"]')
+ ?.focus()
+ })
+ }
+
+ document.addEventListener("pointerdown", handlePointerDown)
+ document.addEventListener("focusin", handleFocusIn)
+ document.addEventListener("keydown", handleKeyDown)
+
+ return () => {
+ document.removeEventListener("pointerdown", handlePointerDown)
+ document.removeEventListener("focusin", handleFocusIn)
+ document.removeEventListener("keydown", handleKeyDown)
+ if (isMobile) document.body.style.overflow = previousOverflow
+ }
+ }, [isMobile, open])
+
+ function focusMenuItem(position: "first" | "last") {
+ setOpen(true)
+ window.requestAnimationFrame(() => {
+ const items = getMenuItems(menuRef.current)
+ const item = position === "first" ? items[0] : items.at(-1)
+ item?.focus()
+ })
+ }
+
+ function selectLocale(nextLocale: DocLocale) {
+ setPreferredLocale(nextLocale)
+ setLocaleMode("manual")
+ setOpen(false)
+ window.requestAnimationFrame(() => triggerRef.current?.focus())
+ }
+
+ const menu = (
+
+ )
+
+ return (
+
+
+
+ {open &&
+ (isMobile ? (
+ createPortal(
+
+
,
+ document.body
+ )
+ ) : (
+
{menu}
+ ))}
+
+ )
+}
+
+function getMenuItems(root: ParentNode | null) {
+ return Array.from(
+ root?.querySelectorAll('[role="menuitemradio"]') ?? []
+ )
+}
+
+function getLocaleOption(locale: DocLocale) {
+ return localeOptions.find((option) => option.locale === locale)!
+}
diff --git a/docs/src/components/layout.tsx b/docs/src/components/layout.tsx
new file mode 100644
index 0000000..ff60c74
--- /dev/null
+++ b/docs/src/components/layout.tsx
@@ -0,0 +1,94 @@
+import { Icon } from "@icones/react"
+import { cn } from "../lib/utils"
+import { useTranslate } from "../lib/locale"
+import React from "react"
+import { SearchTrigger } from "./search-trigger"
+import { SettingsLink } from "./settings-link"
+
+// oxlint-disable-next-line react/only-export-components -- Shared layout tokens are consumed by App.
+export const rootCSSVariables = {
+ "--cw": "60rem",
+ "--max-lw": "18rem",
+ "--max-rw": "15rem",
+ "--min-lw": "12rem",
+ "--min-rw": "10rem",
+ "--w": "calc(var(--max-lw) + var(--cw) + var(--max-rw))",
+ "--page-pl": "calc(2rem + max(0px, (100vw - var(--w)) * 6 / 11 - 1px))",
+ "--page-pr": "calc(2rem + max(0px, 100vw - var(--w)) * 5 / 11)",
+} as const as React.CSSProperties
+
+export function Container({ className, ...rest }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export function HomeButton({
+ href = "/",
+ label,
+}: {
+ href?: string
+ label?: string
+}) {
+ const t = useTranslate()
+
+ return (
+
+
+ {label ?? t({ zh: "首页", us: "Home" })}
+
+ )
+}
+
+export function BackButton({
+ href,
+ children,
+}: {
+ href?: string
+ children?: React.ReactNode
+}) {
+ const t = useTranslate()
+
+ function handleClick(e: React.MouseEvent) {
+ if (!href) {
+ e.preventDefault()
+ history.back()
+ }
+ }
+
+ return (
+
+
+
+
+ {children ?? {t({ zh: "返回", us: "Back" })}}
+
+ )
+}
+
+export function SlashLine({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+export function Actions({ className }: { className?: string }) {
+ return (
+
+
+
+
+ )
+}
diff --git a/docs/src/components/locale-flag.tsx b/docs/src/components/locale-flag.tsx
new file mode 100644
index 0000000..a2bde33
--- /dev/null
+++ b/docs/src/components/locale-flag.tsx
@@ -0,0 +1,17 @@
+import { Icon } from "@icones/react"
+
+import type { DocLocale } from "../content/types"
+
+export function LocaleFlag({
+ className,
+ locale,
+}: {
+ className?: string
+ locale: DocLocale
+}) {
+ return locale === "zh-Hans" ? (
+
+ ) : (
+
+ )
+}
diff --git a/docs/src/components/locale-provider.tsx b/docs/src/components/locale-provider.tsx
new file mode 100644
index 0000000..c7122b8
--- /dev/null
+++ b/docs/src/components/locale-provider.tsx
@@ -0,0 +1,20 @@
+import { useEffect, type ReactNode } from "react"
+
+import type { DocLocale } from "../content/types"
+import { LocaleContext } from "../lib/locale"
+
+export function LocaleProvider({
+ children,
+ locale,
+}: {
+ children: ReactNode
+ locale: DocLocale
+}) {
+ useEffect(() => {
+ document.documentElement.lang = locale
+ }, [locale])
+
+ return (
+ {children}
+ )
+}
diff --git a/docs/src/components/mdx.tsx b/docs/src/components/mdx.tsx
new file mode 100644
index 0000000..05b5f5c
--- /dev/null
+++ b/docs/src/components/mdx.tsx
@@ -0,0 +1,93 @@
+import type { MdxComponents } from "../content/types"
+
+/* oxlint-disable react/only-export-components -- This module exports an MDX component registry. */
+export const mdxComponents: MdxComponents = {
+ a: MdxAnchor,
+ blockquote: MdxBlockquote,
+ code: MdxCode,
+ h2: MdxHeading2,
+ h3: MdxHeading3,
+ pre: MdxPre,
+}
+
+function MdxHeading2({ children, id, ...props }: React.ComponentProps<"h2">) {
+ return (
+
+ )
+}
+
+function MdxHeading3({ children, id, ...props }: React.ComponentProps<"h3">) {
+ return (
+
+ )
+}
+
+function MdxAnchor({ className, ...props }: React.ComponentProps<"a">) {
+ return (
+
+ )
+}
+
+function MdxBlockquote({
+ className,
+ ...props
+}: React.ComponentProps<"blockquote">) {
+ return (
+
+ )
+}
+
+function MdxCode({ className, ...props }: React.ComponentProps<"code">) {
+ return (
+
+ )
+}
+
+function MdxPre({ className, ...props }: React.ComponentProps<"pre">) {
+ return (
+
+ )
+}
+/* oxlint-enable react/only-export-components */
diff --git a/docs/src/components/preferences-provider.tsx b/docs/src/components/preferences-provider.tsx
new file mode 100644
index 0000000..de90e10
--- /dev/null
+++ b/docs/src/components/preferences-provider.tsx
@@ -0,0 +1,228 @@
+import {
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useState,
+ type MutableRefObject,
+ type ReactNode,
+} from "react"
+import {
+ PreferencesProvider,
+ usePreference,
+ usePreferences,
+ type PreferenceChangeHandler,
+ type PreferenceUpdate,
+ type Preferences,
+} from "@workspace/preferences"
+
+import type { DocLocale } from "../content/types"
+import { navigateToLocale } from "../lib/locale-navigation"
+import {
+ detectLocale,
+ docsPreferenceEffects,
+ isDocsPreferenceUpdate,
+ parseDocsPreferences,
+ persistDocsPreference,
+ readDocsPreferences,
+} from "../lib/preferences"
+
+const preferencesChannelName = "docs-preferences-sync"
+
+type PreferencesMessage =
+ | { type: "request" }
+ | { preferences: unknown; type: "snapshot" }
+ | { type: "update"; update: unknown }
+
+export function DocsPreferencesProvider({
+ children,
+ routeLocale,
+}: {
+ children: ReactNode
+ routeLocale: DocLocale
+}) {
+ const [initialPreferences] = useState(() => readDocsPreferences(routeLocale))
+ const channelRef = useRef(null)
+ const applyingExternalUpdateRef = useRef(false)
+
+ const handlePreferenceChange = useCallback(
+ (update) => {
+ persistDocsPreference(update)
+ if (!applyingExternalUpdateRef.current) {
+ channelRef.current?.postMessage({ type: "update", update })
+ }
+ },
+ []
+ )
+
+ return (
+
+
+ {children}
+
+ )
+}
+
+function PreferencesRuntime({
+ applyingExternalUpdateRef,
+ channelRef,
+ routeLocale,
+}: {
+ applyingExternalUpdateRef: MutableRefObject
+ channelRef: MutableRefObject
+ routeLocale: DocLocale
+}) {
+ const preferences = usePreferences()
+ const preferencesRef = useRef(preferences)
+ const [, setAccentColor] = usePreference("accent-color")
+ const [, setBackgroundShade] = usePreference("background-shade")
+ const [, setForegroundShade] = usePreference("foreground-shade")
+ const [, setLocaleMode] = usePreference("locale-mode")
+ const [, setLocale] = usePreference("locale")
+ const [, setThemeMode] = usePreference("theme-mode")
+
+ useLayoutEffect(() => {
+ preferencesRef.current = preferences
+ }, [preferences])
+
+ useEffect(() => {
+ if (!("BroadcastChannel" in window)) return
+
+ const channel = new BroadcastChannel(preferencesChannelName)
+ channelRef.current = channel
+
+ function applyUpdate(update: PreferenceUpdate) {
+ applyingExternalUpdateRef.current = true
+ try {
+ switch (update.key) {
+ case "accent-color":
+ setAccentColor(update.value)
+ break
+ case "background-shade":
+ setBackgroundShade(update.value)
+ break
+ case "foreground-shade":
+ setForegroundShade(update.value)
+ break
+ case "locale-mode":
+ setLocaleMode(update.value)
+ break
+ case "locale":
+ setLocale(update.value)
+ break
+ case "theme-mode":
+ setThemeMode(update.value)
+ break
+ }
+ } finally {
+ applyingExternalUpdateRef.current = false
+ }
+ }
+
+ function applySnapshot(nextPreferences: Preferences) {
+ preferencesRef.current = nextPreferences
+ for (const [key, value] of Object.entries(nextPreferences)) {
+ const update = { key, value }
+ if (isDocsPreferenceUpdate(update)) applyUpdate(update)
+ }
+ }
+
+ function synchronizeRoute(nextPreferences: Preferences) {
+ const locale =
+ nextPreferences["locale-mode"] === "auto"
+ ? detectLocale()
+ : nextPreferences.locale
+ navigateToLocale(locale, true)
+ }
+
+ function handleMessage(event: MessageEvent) {
+ const message = event.data as Partial | undefined
+ if (!message || typeof message !== "object") return
+
+ if (message.type === "request") {
+ channel.postMessage({
+ type: "snapshot",
+ preferences: preferencesRef.current,
+ } satisfies PreferencesMessage)
+ return
+ }
+
+ if (message.type === "update" && isDocsPreferenceUpdate(message.update)) {
+ const nextPreferences = {
+ ...preferencesRef.current,
+ [message.update.key]: message.update.value,
+ }
+ preferencesRef.current = nextPreferences
+ applyUpdate(message.update)
+ synchronizeRoute(nextPreferences)
+ return
+ }
+
+ if (message.type === "snapshot") {
+ const nextPreferences = parseDocsPreferences(message.preferences)
+ if (!nextPreferences) return
+ applySnapshot(nextPreferences)
+ synchronizeRoute(nextPreferences)
+ }
+ }
+
+ channel.addEventListener("message", handleMessage)
+ channel.postMessage({ type: "request" } satisfies PreferencesMessage)
+
+ return () => {
+ channelRef.current = null
+ channel.close()
+ }
+ }, [
+ applyingExternalUpdateRef,
+ channelRef,
+ setAccentColor,
+ setBackgroundShade,
+ setForegroundShade,
+ setLocale,
+ setLocaleMode,
+ setThemeMode,
+ ])
+
+ useEffect(() => {
+ if (preferences["locale-mode"] !== "auto") return
+
+ function synchronizeDetectedLocale() {
+ navigateToLocale(detectLocale(), true)
+ }
+
+ function handleVisibilityChange() {
+ if (document.visibilityState === "visible") synchronizeDetectedLocale()
+ }
+
+ synchronizeDetectedLocale()
+ window.addEventListener("focus", synchronizeDetectedLocale)
+ window.addEventListener("languagechange", synchronizeDetectedLocale)
+ document.addEventListener("visibilitychange", handleVisibilityChange)
+
+ return () => {
+ window.removeEventListener("focus", synchronizeDetectedLocale)
+ window.removeEventListener("languagechange", synchronizeDetectedLocale)
+ document.removeEventListener("visibilitychange", handleVisibilityChange)
+ }
+ }, [preferences])
+
+ useEffect(() => {
+ if (
+ preferences["locale-mode"] === "manual" &&
+ preferences.locale !== routeLocale
+ ) {
+ setLocale(routeLocale)
+ }
+ }, [preferences, routeLocale, setLocale])
+
+ return null
+}
diff --git a/docs/src/components/search-trigger.tsx b/docs/src/components/search-trigger.tsx
new file mode 100644
index 0000000..0fe2e0d
--- /dev/null
+++ b/docs/src/components/search-trigger.tsx
@@ -0,0 +1,24 @@
+import { Icon } from "@icones/react"
+import { SearchTrigger as RawSearchTrigger } from "@workspace/search"
+
+import { useTranslate } from "../lib/locale"
+
+export function SearchTrigger() {
+ const t = useTranslate()
+ const label = t({ zh: "搜索文档", us: "Search documentation" })
+
+ return (
+
+
+ {label}
+
+ ⌘K
+
+
+ )
+}
diff --git a/docs/src/components/settings-link.tsx b/docs/src/components/settings-link.tsx
new file mode 100644
index 0000000..92c33fd
--- /dev/null
+++ b/docs/src/components/settings-link.tsx
@@ -0,0 +1,39 @@
+import { Icon } from "@icones/react"
+import type { ComponentProps } from "react"
+
+import { getLocalizedHref } from "../content/registry"
+import { useLocale, useTranslate } from "../lib/locale"
+import { cn } from "../lib/utils"
+
+export type SettingsLinkProps = Omit<
+ ComponentProps<"a">,
+ "children" | "href"
+> & {
+ label?: string
+}
+
+export function SettingsLink({
+ className,
+ label,
+ ...props
+}: SettingsLinkProps) {
+ const locale = useLocale()
+ const t = useTranslate()
+ const accessibleLabel = label ?? t({ zh: "设置", us: "Settings" })
+
+ return (
+
+
+ {accessibleLabel}
+
+ )
+}
diff --git a/docs/src/components/swatch.tsx b/docs/src/components/swatch.tsx
new file mode 100644
index 0000000..27998e7
--- /dev/null
+++ b/docs/src/components/swatch.tsx
@@ -0,0 +1,48 @@
+import { Icon } from "@icones/react"
+import type { ComponentProps } from "react"
+
+import { cn } from "../lib/utils"
+
+export type SwatchProps = Omit, "children"> & {
+ light: string
+ dark?: string
+ selected?: boolean
+}
+
+export function Swatch({
+ className,
+ light,
+ dark = light,
+ selected = false,
+ ...props
+}: SwatchProps) {
+ return (
+
+ )
+}
diff --git a/docs/src/components/theme-switch.tsx b/docs/src/components/theme-switch.tsx
new file mode 100644
index 0000000..297b98a
--- /dev/null
+++ b/docs/src/components/theme-switch.tsx
@@ -0,0 +1,55 @@
+import { Icon } from "@icones/react"
+import { usePreference } from "@workspace/preferences"
+
+import { useResolvedTheme } from "../lib/appearance"
+import { useTranslate } from "../lib/locale"
+
+export function ThemeSwitch({ className }: { className?: string }) {
+ const t = useTranslate()
+ const [, setThemeMode] = usePreference("theme-mode")
+ const resolvedTheme = useResolvedTheme()
+ const dark = resolvedTheme === "dark"
+ const label = dark
+ ? t({ zh: "切换到浅色主题", us: "Switch to light theme" })
+ : t({ zh: "切换到深色主题", us: "Switch to dark theme" })
+
+ return (
+
+ )
+}
diff --git a/docs/src/content/registry.ts b/docs/src/content/registry.ts
new file mode 100644
index 0000000..b9c38fd
--- /dev/null
+++ b/docs/src/content/registry.ts
@@ -0,0 +1,355 @@
+import {
+ defaultDocLocale,
+ docLocales,
+ docSections,
+ type DocLocale,
+ type DocLocation,
+ type DocPackage,
+ type DocPage,
+ type DocSection,
+ type DocTocItem,
+ type MdxComponents,
+} from "./types"
+import { trans, type Translation } from "../lib/locale"
+
+type DocModule = {
+ default: React.ComponentType<{ components?: MdxComponents }>
+ frontmatter: Record
+}
+
+type LocalizedText = Translation
+
+type PackageDefinition = {
+ key: string
+ label: string
+ description: LocalizedText
+}
+
+type ParsedDocPage = DocPage & {
+ packageKey: string
+ searchText: string
+}
+
+const packageDefinitions: readonly PackageDefinition[] = [
+ {
+ key: "blocks",
+ label: "Blocks",
+ description: {
+ zh: "构建应用外壳,并接入导航、通知、聊天、媒体和外观控制。",
+ us: "Build an application shell and add navigation, notifications, chat, media, and appearance controls.",
+ },
+ },
+ {
+ key: "i18n",
+ label: "I18n",
+ description: {
+ zh: "接入应用自有词典、本地化运行时、开发工具和完整翻译工作流。",
+ us: "Add application-owned catalogs, localized runtime rendering, development tools, and translation workflows.",
+ },
+ },
+ {
+ key: "lexical",
+ label: "Lexical",
+ description: {
+ zh: "从根节点和内容区域开始,逐步构建支持自定义操作与媒体的富文本编辑器。",
+ us: "Build a rich-text editor progressively from its root and content surface to custom actions and media.",
+ },
+ },
+ {
+ key: "preferences",
+ label: "Preferences",
+ description: {
+ zh: "使用 SSR 快照、持久化回调和可组合副作用管理类型安全且可扩展的应用偏好。",
+ us: "Manage typed, extensible application preferences with SSR snapshots, persistence callbacks, and composable effects.",
+ },
+ },
+ {
+ key: "search",
+ label: "Search",
+ description: {
+ zh: "通过可替换的 Adapter、分组结果、分页和搜索历史构建应用级搜索体验。",
+ us: "Build application search with a replaceable adapter, grouped results, pagination, and search history.",
+ },
+ },
+ {
+ key: "ui",
+ label: "UI",
+ description: {
+ zh: "配置共享 UI 层,并学习组合组件、Hooks、图标和本地化词典。",
+ us: "Set up the shared UI layer and learn how to compose its components, hooks, icons, and locale catalogs.",
+ },
+ },
+]
+
+const docModules = import.meta.glob(
+ "../../../packages/*/docs/*/**/*.mdx",
+ { eager: true }
+)
+
+const docSources = import.meta.glob(
+ "../../../packages/*/docs/*/**/*.mdx",
+ { eager: true, import: "default", query: "?docs-search-raw" }
+)
+
+const parsedPages = Object.entries(docModules).map(([path, module]) =>
+ parseDocPage(path, module, docSources[path] ?? "")
+)
+
+const packagesByLocale: Record = {
+ "zh-Hans": packageDefinitions.map((definition) =>
+ createDocPackage(definition, "zh-Hans")
+ ),
+ "en-US": packageDefinitions.map((definition) =>
+ createDocPackage(definition, "en-US")
+ ),
+}
+
+validateLocalizedPagePairs(packagesByLocale)
+
+export function getDocPackages(locale: DocLocale = defaultDocLocale) {
+ return packagesByLocale[locale]
+}
+
+export function getDocHref(docPackage: DocPackage, page?: DocPage) {
+ const prefix = getLocalePrefix(docPackage.locale)
+ const target = page ?? getOverviewPage(docPackage)
+ const pagePath =
+ target.section === "overview"
+ ? ""
+ : `/${target.section}/${encodeURIComponent(target.slug)}`
+
+ return `${prefix}/articles/${docPackage.key}${pagePath}`
+}
+
+export function getDocsHomeHref(locale: DocLocale) {
+ return getLocalePrefix(locale) || "/"
+}
+
+export function getLocalizedHref(pathname: string, locale: DocLocale) {
+ if (/^(?:\/(?:en-US|zh-Hans))?\/settings\/?$/.test(pathname)) {
+ return `${getLocalePrefix(locale)}/settings`
+ }
+
+ const location = resolveDocLocation(pathname)
+ if (!location) return getDocsHomeHref(locale)
+
+ const docPackage = getDocPackages(locale).find(
+ (item) => item.key === location.package.key
+ )
+ const page = docPackage?.pages.find(
+ (item) =>
+ item.section === location.page.section && item.slug === location.page.slug
+ )
+
+ return docPackage && page
+ ? getDocHref(docPackage, page)
+ : getDocsHomeHref(locale)
+}
+
+export function resolveDocLocale(pathname: string): DocLocale {
+ const firstSegment = pathname.split("/").filter(Boolean)[0]
+ return isDocLocale(firstSegment) ? firstSegment : defaultDocLocale
+}
+
+export function resolveDocLocation(pathname: string): DocLocation | undefined {
+ const segments = pathname.split("/").filter(Boolean)
+ let locale: DocLocale = defaultDocLocale
+ if (isDocLocale(segments[0])) {
+ locale = segments.shift() as DocLocale
+ }
+ const [resource, packageKey, sectionValue, pageSlug, ...extra] = segments
+
+ if (resource !== "articles" || extra.length > 0) return undefined
+
+ const docPackage = getDocPackages(locale).find(
+ (item) => item.key === packageKey
+ )
+ if (!docPackage) return undefined
+
+ if (sectionValue === undefined) {
+ const page = getOverviewPage(docPackage)
+ return { locale, package: docPackage, page }
+ }
+
+ if (sectionValue === "overview" || !isDocSection(sectionValue) || !pageSlug) {
+ return undefined
+ }
+
+ const page = docPackage.pages.find(
+ (item) => item.section === sectionValue && item.slug === pageSlug
+ )
+
+ return page ? { locale, package: docPackage, page } : undefined
+}
+
+export function getDocSectionLabel(section: DocSection, locale: DocLocale) {
+ const labels: Record = {
+ overview: { zh: "概览", us: "Overview" },
+ guide: { zh: "指南", us: "Guide" },
+ advanced: { zh: "进阶", us: "Advanced" },
+ examples: { zh: "示例", us: "Examples" },
+ }
+
+ return trans(locale, labels[section])
+}
+
+export function getDocSearchText(page: DocPage) {
+ return (page as ParsedDocPage).searchText
+}
+
+function createDocPackage(
+ definition: PackageDefinition,
+ locale: DocLocale
+): DocPackage {
+ const pages = parsedPages
+ .filter(
+ (page) => page.packageKey === definition.key && page.locale === locale
+ )
+ .sort(
+ (left, right) =>
+ docSections.indexOf(left.section) -
+ docSections.indexOf(right.section) ||
+ left.order - right.order ||
+ left.slug.localeCompare(right.slug)
+ )
+
+ const overviewCount = pages.filter(
+ (page) => page.section === "overview"
+ ).length
+ if (overviewCount !== 1) {
+ throw new Error(
+ `${definition.key}/${locale} must contain exactly one docs/${locale}/overview.mdx file.`
+ )
+ }
+
+ for (const section of docSections.slice(1)) {
+ if (!pages.some((page) => page.section === section)) {
+ throw new Error(
+ `${definition.key}/${locale} must contain at least one ${section} document.`
+ )
+ }
+ }
+
+ return {
+ key: definition.key,
+ label: definition.label,
+ description: trans(locale, definition.description),
+ locale,
+ pages,
+ }
+}
+
+function parseDocPage(
+ path: string,
+ module: DocModule,
+ source: string
+): ParsedDocPage {
+ const match = path.match(
+ /\/packages\/([^/]+)\/docs\/([^/]+)\/(overview|guide|advanced|examples)(?:\/([^/]+))?\.mdx$/
+ )
+ if (!match) {
+ throw new Error(
+ `Invalid documentation path "${path}". Expected docs//overview.mdx or docs///*.mdx.`
+ )
+ }
+
+ const [, packageKey, localeValue, sectionValue, nestedSlug] = match
+ if (!isDocLocale(localeValue)) {
+ throw new Error(
+ `Unsupported documentation locale "${localeValue}" in "${path}".`
+ )
+ }
+ if (!isDocSection(sectionValue)) {
+ throw new Error(`Unsupported documentation section in "${path}".`)
+ }
+ if (
+ (sectionValue === "overview" && nestedSlug !== undefined) ||
+ (sectionValue !== "overview" && nestedSlug === undefined)
+ ) {
+ throw new Error(`Documentation file is in the wrong directory: "${path}".`)
+ }
+
+ const frontmatter = module.frontmatter
+ return {
+ Content: module.default,
+ description: readString(frontmatter, "description"),
+ locale: localeValue,
+ order: readNumber(frontmatter, "order"),
+ packageKey,
+ searchText: source,
+ section: sectionValue,
+ slug: nestedSlug ?? "overview",
+ title: readString(frontmatter, "title"),
+ toc: readToc(frontmatter.toc),
+ }
+}
+
+function validateLocalizedPagePairs(
+ packages: Record
+) {
+ for (const definition of packageDefinitions) {
+ const keysByLocale = docLocales.map((locale) => {
+ const docPackage = packages[locale].find(
+ (item) => item.key === definition.key
+ )!
+ return docPackage.pages.map((page) => `${page.section}/${page.slug}`)
+ })
+
+ if (keysByLocale[0]?.join("\n") !== keysByLocale[1]?.join("\n")) {
+ throw new Error(
+ `${definition.key} documentation must contain matching zh-Hans and en-US page paths.`
+ )
+ }
+ }
+}
+
+function getOverviewPage(docPackage: DocPackage) {
+ return docPackage.pages.find((page) => page.section === "overview")!
+}
+
+function getLocalePrefix(locale: DocLocale) {
+ return locale === defaultDocLocale ? "" : `/${locale}`
+}
+
+function isDocLocale(value: unknown): value is DocLocale {
+ return docLocales.some((locale) => locale === value)
+}
+
+function isDocSection(value: unknown): value is DocSection {
+ return docSections.some((section) => section === value)
+}
+
+function readString(frontmatter: Record, key: string) {
+ const value = frontmatter[key]
+ if (typeof value === "string" && value.trim()) return value
+ throw new Error(`Documentation frontmatter requires a string "${key}".`)
+}
+
+function readNumber(frontmatter: Record, key: string) {
+ const value = frontmatter[key]
+ if (typeof value === "number" && Number.isFinite(value)) return value
+ throw new Error(`Documentation frontmatter requires a number "${key}".`)
+}
+
+function readToc(value: unknown): readonly DocTocItem[] {
+ if (value === undefined) return []
+ if (!Array.isArray(value)) {
+ throw new Error('Documentation frontmatter "toc" must be an array.')
+ }
+
+ return value.map((item) => {
+ if (!isRecord(item)) {
+ throw new Error('Each documentation "toc" entry must be an object.')
+ }
+
+ return {
+ id: readString(item, "id"),
+ title: readString(item, "title"),
+ items: item.items === undefined ? undefined : readToc(item.items),
+ }
+ })
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null
+}
diff --git a/docs/src/content/types.ts b/docs/src/content/types.ts
new file mode 100644
index 0000000..d1915c8
--- /dev/null
+++ b/docs/src/content/types.ts
@@ -0,0 +1,45 @@
+import type { ComponentType, ElementType } from "react"
+
+export const docLocales = ["zh-Hans", "en-US"] as const
+export const defaultDocLocale = "zh-Hans" as const
+export const docSections = [
+ "overview",
+ "guide",
+ "advanced",
+ "examples",
+] as const
+
+export type DocLocale = (typeof docLocales)[number]
+export type DocSection = (typeof docSections)[number]
+export type MdxComponents = Record
+
+export type DocTocItem = {
+ id: string
+ title: string
+ items?: readonly DocTocItem[]
+}
+
+export type DocPage = {
+ Content: ComponentType<{ components?: MdxComponents }>
+ description: string
+ locale: DocLocale
+ order: number
+ section: DocSection
+ slug: string
+ title: string
+ toc: readonly DocTocItem[]
+}
+
+export type DocPackage = {
+ key: string
+ label: string
+ description: string
+ locale: DocLocale
+ pages: readonly DocPage[]
+}
+
+export type DocLocation = {
+ locale: DocLocale
+ package: DocPackage
+ page: DocPage
+}
diff --git a/docs/src/index.css b/docs/src/index.css
new file mode 100644
index 0000000..06abf2f
--- /dev/null
+++ b/docs/src/index.css
@@ -0,0 +1,86 @@
+@import "@workspace/ui/globals.css";
+@import "@workspace/search/globals.css";
+
+@custom-variant dark (&:where(.dark, .dark *));
+
+@layer base {
+ *,
+ *::before,
+ *::after {
+ @apply border-slate-200 dark:border-slate-800;
+ }
+
+ html {
+ @apply antialiased;
+ background-color: var(--docs-background-light, var(--color-white));
+ color: var(--docs-foreground-light, var(--color-slate-950));
+ font-family: "DM Serif Display", Georgia, serif;
+ }
+
+ html.dark {
+ background-color: var(--docs-background-dark, var(--color-slate-950));
+ color: var(--docs-foreground-dark, var(--color-slate-100));
+ }
+
+ :root {
+ --background: var(--docs-background-light, var(--color-white));
+ --foreground: var(--docs-foreground-light, var(--color-slate-950));
+ --card: var(--docs-background-light, var(--color-white));
+ --card-foreground: var(--docs-foreground-light, var(--color-slate-950));
+ --popover: var(--docs-background-light, var(--color-white));
+ --popover-foreground: var(--docs-foreground-light, var(--color-slate-950));
+ --primary: var(--color-indigo-600);
+ --primary-foreground: var(--color-white);
+ --secondary: var(--docs-surface-light, var(--color-slate-100));
+ --secondary-foreground: var(
+ --docs-foreground-light,
+ var(--color-slate-950)
+ );
+ --muted: var(--docs-surface-light, var(--color-slate-100));
+ --muted-foreground: var(--docs-muted-light, var(--color-slate-600));
+ --accent: var(--docs-surface-light, var(--color-slate-100));
+ --accent-foreground: var(--docs-foreground-light, var(--color-slate-950));
+ --border: var(--color-slate-200);
+ --input: var(--color-slate-200);
+ --ring: var(--color-indigo-500);
+ --sidebar: var(--docs-background-light, var(--color-white));
+ --sidebar-foreground: var(--docs-foreground-light, var(--color-slate-950));
+ --sidebar-border: var(--color-slate-200);
+ }
+
+ html.dark {
+ --background: var(--docs-background-dark, var(--color-slate-950));
+ --foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --card: var(--docs-surface-dark, var(--color-slate-900));
+ --card-foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --popover: var(--docs-surface-dark, var(--color-slate-900));
+ --popover-foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --primary: var(--color-indigo-400);
+ --primary-foreground: var(--color-slate-950);
+ --secondary: var(--color-slate-800);
+ --secondary-foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --muted: var(--color-slate-800);
+ --muted-foreground: var(--docs-muted-dark, var(--color-slate-400));
+ --accent: var(--color-slate-800);
+ --accent-foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --border: var(--color-slate-800);
+ --input: var(--color-slate-700);
+ --ring: var(--color-indigo-400);
+ --sidebar: var(--docs-surface-dark, var(--color-slate-900));
+ --sidebar-foreground: var(--docs-foreground-dark, var(--color-slate-100));
+ --sidebar-border: var(--color-slate-800);
+ }
+
+ body,
+ #root {
+ @apply min-h-dvh;
+ }
+
+ body {
+ @apply bg-inherit text-inherit;
+ }
+
+ ::selection {
+ @apply bg-indigo-200 text-indigo-950 dark:bg-indigo-500/40 dark:text-indigo-50;
+ }
+}
diff --git a/docs/src/lib/appearance.ts b/docs/src/lib/appearance.ts
new file mode 100644
index 0000000..44bae22
--- /dev/null
+++ b/docs/src/lib/appearance.ts
@@ -0,0 +1,155 @@
+import { useCallback, useSyncExternalStore } from "react"
+import { usePreference } from "@workspace/preferences"
+import colors from "tailwindcss/colors"
+
+export const themeModes = ["light", "dark", "system"] as const
+export const accentColors = [
+ "neutral",
+ "blue",
+ "cyan",
+ "emerald",
+ "fuchsia",
+ "green",
+ "indigo",
+ "lime",
+ "orange",
+ "pink",
+ "purple",
+ "red",
+ "rose",
+ "sky",
+ "teal",
+ "violet",
+ "yellow",
+] as const
+export const backgroundShades = [
+ "neutral",
+ "stone",
+ "zinc",
+ "mauve",
+ "olive",
+ "mist",
+ "taupe",
+] as const
+export const foregroundShades = ["slate", "neutral", "stone"] as const
+
+export type ThemeMode = (typeof themeModes)[number]
+export type AccentColor = (typeof accentColors)[number]
+export type BackgroundShade = (typeof backgroundShades)[number]
+export type ForegroundShade = (typeof foregroundShades)[number]
+export type ResolvedTheme = Exclude
+
+export type Appearance = {
+ accentColor: AccentColor
+ backgroundShade: BackgroundShade
+ foregroundShade: ForegroundShade
+ themeMode: ThemeMode
+}
+
+export type AppearanceController = Appearance & {
+ updateAppearance: (update: Partial) => void
+}
+
+const systemThemeQuery = "(prefers-color-scheme: dark)"
+const colorSteps = [
+ 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950,
+] as const
+
+type ColorStep = (typeof colorSteps)[number]
+type ColorScale = Record
+
+export function useAppearance(): AppearanceController {
+ const [accentColor, setAccentColor] = usePreference("accent-color")
+ const [backgroundShade, setBackgroundShade] =
+ usePreference("background-shade")
+ const [foregroundShade, setForegroundShade] =
+ usePreference("foreground-shade")
+ const [themeMode, setThemeMode] = usePreference("theme-mode")
+
+ const updateAppearance = useCallback(
+ (update: Partial) => {
+ if (update.accentColor !== undefined) {
+ setAccentColor(update.accentColor)
+ }
+ if (update.backgroundShade !== undefined) {
+ setBackgroundShade(update.backgroundShade)
+ }
+ if (update.foregroundShade !== undefined) {
+ setForegroundShade(update.foregroundShade)
+ }
+ if (update.themeMode !== undefined) setThemeMode(update.themeMode)
+ },
+ [setAccentColor, setBackgroundShade, setForegroundShade, setThemeMode]
+ )
+
+ return {
+ accentColor,
+ backgroundShade,
+ foregroundShade,
+ themeMode,
+ updateAppearance,
+ }
+}
+
+export function useResolvedTheme() {
+ const [themeMode] = usePreference("theme-mode")
+ const systemTheme = useSyncExternalStore(
+ subscribeToSystemTheme,
+ getSystemTheme,
+ () => "light" as const
+ )
+
+ return themeMode === "system" ? systemTheme : themeMode
+}
+
+export function getPaletteColor(
+ color: AccentColor | BackgroundShade | ForegroundShade,
+ step: ColorStep = 500
+) {
+ return getColorScale(color)[step]
+}
+
+export function applyAppearance(appearance: Appearance) {
+ if (typeof document === "undefined") return
+
+ const root = document.documentElement
+ const resolvedTheme =
+ appearance.themeMode === "system" ? getSystemTheme() : appearance.themeMode
+ const accentScale = getColorScale(appearance.accentColor)
+ const backgroundScale = getColorScale(appearance.backgroundShade)
+ const foregroundScale = getColorScale(appearance.foregroundShade)
+
+ root.classList.toggle("dark", resolvedTheme === "dark")
+ root.style.colorScheme = resolvedTheme
+ root.dataset.themeMode = appearance.themeMode
+ root.dataset.accentColor = appearance.accentColor
+ root.dataset.backgroundShade = appearance.backgroundShade
+ root.dataset.foregroundShade = appearance.foregroundShade
+
+ for (const step of colorSteps) {
+ root.style.setProperty(`--color-indigo-${step}`, accentScale[step])
+ }
+
+ root.style.setProperty("--docs-background-light", backgroundScale[50])
+ root.style.setProperty("--docs-background-dark", backgroundScale[950])
+ root.style.setProperty("--docs-surface-light", backgroundScale[100])
+ root.style.setProperty("--docs-surface-dark", backgroundScale[900])
+ root.style.setProperty("--docs-foreground-light", foregroundScale[950])
+ root.style.setProperty("--docs-foreground-dark", foregroundScale[50])
+ root.style.setProperty("--docs-muted-light", foregroundScale[600])
+ root.style.setProperty("--docs-muted-dark", foregroundScale[400])
+}
+
+export function subscribeToSystemTheme(listener: VoidFunction) {
+ const mediaQuery = window.matchMedia(systemThemeQuery)
+ mediaQuery.addEventListener("change", listener)
+ return () => mediaQuery.removeEventListener("change", listener)
+}
+
+export function getSystemTheme(): ResolvedTheme {
+ return window.matchMedia(systemThemeQuery).matches ? "dark" : "light"
+}
+
+function getColorScale(color: string) {
+ return colors[color as keyof typeof colors] as ColorScale
+}
diff --git a/docs/src/lib/locale-navigation.ts b/docs/src/lib/locale-navigation.ts
new file mode 100644
index 0000000..b0ce217
--- /dev/null
+++ b/docs/src/lib/locale-navigation.ts
@@ -0,0 +1,14 @@
+import { getLocalizedHref } from "../content/registry"
+import type { DocLocale } from "../content/types"
+
+export function navigateToLocale(locale: DocLocale, replace = false) {
+ const href = getLocalizedHref(window.location.pathname, locale)
+ if (href === window.location.pathname) return
+
+ if (replace) {
+ window.history.replaceState(null, "", href)
+ } else {
+ window.history.pushState(null, "", href)
+ }
+ window.dispatchEvent(new PopStateEvent("popstate"))
+}
diff --git a/docs/src/lib/locale.ts b/docs/src/lib/locale.ts
new file mode 100644
index 0000000..c1bdd33
--- /dev/null
+++ b/docs/src/lib/locale.ts
@@ -0,0 +1,32 @@
+import { createContext, useCallback, useContext } from "react"
+
+import { defaultDocLocale, type DocLocale } from "../content/types"
+
+export type Translation = Readonly<{
+ zh: Value
+ us: Value
+}>
+
+export type Translate = (translation: Translation) => Value
+
+export const LocaleContext = createContext(defaultDocLocale)
+
+export function useLocale() {
+ return useContext(LocaleContext)
+}
+
+export function useTranslate(): Translate {
+ const locale = useLocale()
+
+ return useCallback(
+ (translation: Translation) => trans(locale, translation),
+ [locale]
+ )
+}
+
+export function trans(
+ locale: DocLocale,
+ translation: Translation
+): Value {
+ return locale === "zh-Hans" ? translation.zh : translation.us
+}
diff --git a/docs/src/lib/preferences.ts b/docs/src/lib/preferences.ts
new file mode 100644
index 0000000..aaac2d0
--- /dev/null
+++ b/docs/src/lib/preferences.ts
@@ -0,0 +1,188 @@
+import {
+ createPreferenceUpdateGuard,
+ type PreferenceDefinitions,
+ type PreferenceEffect,
+ type PreferenceUpdate,
+ type Preferences,
+} from "@workspace/preferences"
+
+import { defaultDocLocale, docLocales, type DocLocale } from "../content/types"
+import {
+ accentColors,
+ applyAppearance,
+ backgroundShades,
+ foregroundShades,
+ subscribeToSystemTheme,
+ themeModes,
+ type AccentColor,
+ type Appearance,
+ type BackgroundShade,
+ type ForegroundShade,
+ type ThemeMode,
+} from "./appearance"
+
+export type LocaleMode = "auto" | "manual"
+
+export type DocsPreferences = {
+ "accent-color": AccentColor
+ "background-shade": BackgroundShade
+ "foreground-shade": ForegroundShade
+ "locale-mode": LocaleMode
+ locale: DocLocale
+ "theme-mode": ThemeMode
+}
+
+declare module "@workspace/preferences" {
+ interface PreferencesCustom extends DocsPreferences {}
+}
+
+export const docsPreferenceDefinitions = {
+ "accent-color": createStringPreference(
+ "docs-accent-color",
+ "indigo",
+ accentColors
+ ),
+ "background-shade": createStringPreference(
+ "docs-background-shade",
+ "neutral",
+ backgroundShades
+ ),
+ "foreground-shade": createStringPreference(
+ "docs-foreground-shade",
+ "slate",
+ foregroundShades
+ ),
+ "locale-mode": {
+ cookie: "docs-locale-auto-detect",
+ defaultValue: "manual",
+ is: (value): value is LocaleMode => value === "auto" || value === "manual",
+ parse: (value) => (value === "1" || value === "auto" ? "auto" : "manual"),
+ serialize: (value) => (value === "auto" ? "1" : "0"),
+ },
+ locale: createStringPreference("docs-locale", defaultDocLocale, docLocales),
+ "theme-mode": createStringPreference("docs-theme", "system", themeModes),
+} satisfies PreferenceDefinitions
+
+export const isDocsPreferenceUpdate = createPreferenceUpdateGuard(
+ docsPreferenceDefinitions
+)
+
+export const docsPreferenceEffects = [
+ {
+ layoutEffect: ({ preferences }) => {
+ applyAppearance(toAppearance(preferences))
+ },
+ effect: ({ preferences, store }) => {
+ if (preferences["theme-mode"] !== "system") return
+
+ return subscribeToSystemTheme(() => {
+ applyAppearance(toAppearance(store.getSnapshot()))
+ })
+ },
+ },
+] satisfies readonly PreferenceEffect[]
+
+export function initializeDocsPreferences() {
+ applyAppearance(toAppearance(readDocsPreferences()))
+}
+
+export function readDocsPreferences(
+ fallbackLocale: DocLocale = defaultDocLocale
+): Preferences {
+ const preferences = Object.fromEntries(
+ (
+ Object.keys(docsPreferenceDefinitions) as Array
+ ).map((key) => {
+ const definition = docsPreferenceDefinitions[key]
+ return [key, readStoredValue(definition.cookie, definition.parse)]
+ })
+ ) as DocsPreferences
+
+ if (!readStoredString(docsPreferenceDefinitions.locale.cookie)) {
+ preferences.locale = fallbackLocale
+ }
+
+ return preferences
+}
+
+export function persistDocsPreference(update: PreferenceUpdate) {
+ const definition = docsPreferenceDefinitions[update.key]
+ const serializedValue =
+ update.key === "locale-mode"
+ ? docsPreferenceDefinitions["locale-mode"].serialize(update.value)
+ : update.value
+ writeStoredString(definition.cookie, serializedValue)
+}
+
+export function parseDocsPreferences(value: unknown): Preferences | undefined {
+ if (!value || typeof value !== "object") return undefined
+
+ const candidate = value as Partial>
+ for (const key of Object.keys(docsPreferenceDefinitions) as Array<
+ keyof DocsPreferences
+ >) {
+ if (!docsPreferenceDefinitions[key].is(candidate[key])) return undefined
+ }
+
+ return candidate as DocsPreferences
+}
+
+export function toAppearance(preferences: Preferences): Appearance {
+ return {
+ accentColor: preferences["accent-color"],
+ backgroundShade: preferences["background-shade"],
+ foregroundShade: preferences["foreground-shade"],
+ themeMode: preferences["theme-mode"],
+ }
+}
+
+export function detectLocale(): DocLocale {
+ const languages = navigator.languages.length
+ ? navigator.languages
+ : [navigator.language]
+ return languages.some((language) => language.toLowerCase().startsWith("zh"))
+ ? "zh-Hans"
+ : "en-US"
+}
+
+function createStringPreference(
+ cookie: string,
+ defaultValue: Options[number],
+ options: Options
+) {
+ const is = (value: unknown): value is Options[number] =>
+ typeof value === "string" && options.some((option) => option === value)
+
+ return {
+ cookie,
+ defaultValue,
+ is,
+ parse: (value: string | undefined) => (is(value) ? value : defaultValue),
+ serialize: (value: Options[number]) => value,
+ }
+}
+
+function readStoredValue(
+ key: string,
+ parse: (value: string | undefined) => Value
+) {
+ return parse(readStoredString(key))
+}
+
+function readStoredString(key: string) {
+ if (typeof window === "undefined") return undefined
+
+ try {
+ return window.sessionStorage.getItem(key) ?? undefined
+ } catch {
+ return undefined
+ }
+}
+
+function writeStoredString(key: string, value: string) {
+ try {
+ window.sessionStorage.setItem(key, value)
+ } catch {
+ // The in-memory preference remains active when storage is unavailable.
+ }
+}
diff --git a/docs/src/lib/use-mobile.ts b/docs/src/lib/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/docs/src/lib/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/docs/src/lib/utils.ts b/docs/src/lib/utils.ts
new file mode 100644
index 0000000..f584180
--- /dev/null
+++ b/docs/src/lib/utils.ts
@@ -0,0 +1,8 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export type { ClassValue }
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/docs/src/main.tsx b/docs/src/main.tsx
new file mode 100644
index 0000000..42d888b
--- /dev/null
+++ b/docs/src/main.tsx
@@ -0,0 +1,18 @@
+import { StrictMode } from "react"
+import { createRoot } from "react-dom/client"
+
+import App from "./App.tsx"
+import "./index.css"
+import { initializeDocsPreferences } from "./lib/preferences"
+
+document.documentElement.lang = window.location.pathname.startsWith("/en-US")
+ ? "en-US"
+ : "zh-Hans"
+
+initializeDocsPreferences()
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+)
diff --git a/docs/src/pages/article.tsx b/docs/src/pages/article.tsx
new file mode 100644
index 0000000..d6f0bd8
--- /dev/null
+++ b/docs/src/pages/article.tsx
@@ -0,0 +1,704 @@
+import * as React from "react"
+import { createPortal } from "react-dom"
+import { Icon } from "@icones/react"
+import {
+ getDocHref,
+ getDocPackages,
+ getDocSectionLabel,
+ getDocsHomeHref,
+ resolveDocLocation,
+} from "../content/registry"
+import type {
+ DocLocation,
+ DocPage,
+ DocTocItem,
+} from "../content/types"
+import { Actions, Container, HomeButton, SlashLine } from "../components/layout"
+import { mdxComponents } from "../components/mdx"
+import { useLocale, useTranslate } from "../lib/locale"
+
+const noTocItems: readonly DocTocItem[] = []
+
+export default function ArticlePage() {
+ const t = useTranslate()
+ const docLocation = resolveDocLocation(window.location.pathname)
+ const visibleTocIds = useVisibleTocIds(docLocation?.page.toc ?? noTocItems)
+ const [mobilePanel, setMobilePanel] = React.useState<
+ "navigation" | "toc" | null
+ >(null)
+ const mobilePanelRef = React.useRef(null)
+ const navigationTriggerRef = React.useRef(null)
+ const tocTriggerRef = React.useRef(null)
+
+ React.useEffect(() => {
+ if (!mobilePanel) return
+
+ const previousOverflow = document.body.style.overflow
+ document.body.style.overflow = "hidden"
+ const unavailableAt = window.matchMedia(
+ mobilePanel === "navigation" ? "(min-width: 48rem)" : "(min-width: 64rem)"
+ )
+
+ const animationFrame = window.requestAnimationFrame(() => {
+ mobilePanelRef.current
+ ?.querySelector(
+ 'a[href], button:not(:disabled), [tabindex]:not([tabindex="-1"])'
+ )
+ ?.focus()
+ })
+
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ event.preventDefault()
+ setMobilePanel(null)
+ const trigger =
+ mobilePanel === "navigation"
+ ? navigationTriggerRef.current
+ : tocTriggerRef.current
+ trigger?.focus()
+ return
+ }
+
+ if (event.key !== "Tab") return
+
+ const focusableElements = Array.from(
+ mobilePanelRef.current?.querySelectorAll(
+ 'a[href], button:not(:disabled), [tabindex]:not([tabindex="-1"])'
+ ) ?? []
+ )
+ const firstElement = focusableElements[0]
+ const lastElement = focusableElements.at(-1)
+
+ if (!firstElement || !lastElement) return
+
+ if (event.shiftKey && document.activeElement === firstElement) {
+ event.preventDefault()
+ lastElement.focus()
+ } else if (!event.shiftKey && document.activeElement === lastElement) {
+ event.preventDefault()
+ firstElement.focus()
+ }
+ }
+
+ function handleBreakpointChange(event: MediaQueryListEvent) {
+ if (event.matches) setMobilePanel(null)
+ }
+
+ document.addEventListener("keydown", handleKeyDown)
+ unavailableAt.addEventListener("change", handleBreakpointChange)
+
+ return () => {
+ window.cancelAnimationFrame(animationFrame)
+ document.removeEventListener("keydown", handleKeyDown)
+ unavailableAt.removeEventListener("change", handleBreakpointChange)
+ document.body.style.overflow = previousOverflow
+ }
+ }, [mobilePanel])
+
+ if (!docLocation) return
+
+ const currentIndex = docLocation.package.pages.indexOf(docLocation.page)
+ const previousPage = docLocation.package.pages[currentIndex - 1]
+ const nextPage = docLocation.package.pages[currentIndex + 1]
+ const previous = previousPage
+ ? {
+ locale: docLocation.locale,
+ package: docLocation.package,
+ page: previousPage,
+ }
+ : undefined
+ const next = nextPage
+ ? {
+ locale: docLocation.locale,
+ package: docLocation.package,
+ page: nextPage,
+ }
+ : undefined
+
+ return (
+ <>
+
+
+
+
+ {docLocation.package.label}
+
+
+
+
+
+
+
+
+
+ {mobilePanel &&
+ typeof document !== "undefined" &&
+ createPortal(
+
+
{
+ const trigger =
+ mobilePanel === "navigation"
+ ? navigationTriggerRef.current
+ : tocTriggerRef.current
+ setMobilePanel(null)
+ window.requestAnimationFrame(() => trigger?.focus())
+ }}
+ />
+
{
+ if (
+ event.target instanceof Element &&
+ event.target.closest("a[href]")
+ ) {
+ setMobilePanel(null)
+ }
+ }}
+ >
+ {mobilePanel === "navigation" ? (
+
+ ) : (
+
+ )}
+
+
,
+ document.body
+ )}
+
+
+
+ >
+ )
+}
+
+function Sidebar({ current }: { current: DocLocation }) {
+ const overview = current.package.pages.find(
+ (page) => page.section === "overview"
+ )
+ const groups = current.package.pages
+ .filter((page) => page.section !== "overview")
+ .reduce
>(
+ (result, page) => {
+ const group = result.at(-1)
+ if (group?.section === page.section) {
+ group.pages.push(page)
+ } else {
+ result.push({ section: page.section, pages: [page] })
+ }
+ return result
+ },
+ []
+ )
+
+ return (
+
+ {overview && (
+ -
+
+
+ )}
+ {groups.map((group) => (
+ -
+
+ {getDocSectionLabel(group.section, current.locale)}
+
+
+ {group.pages.map((page) => (
+ -
+
+
+ ))}
+
+
+ ))}
+
+ )
+}
+
+function SidebarPageLink({
+ current,
+ page,
+ prominent = false,
+}: {
+ current: DocLocation
+ page: DocPage
+ prominent?: boolean
+}) {
+ return (
+
+ {page.title}
+
+ )
+}
+
+function Header({ docLocation }: { docLocation: DocLocation }) {
+ return (
+
+ )
+}
+
+function ArticleContent({ page }: { page: DocPage }) {
+ const Content = page.Content
+
+ return (
+
+
+
+ )
+}
+
+function TableOfContents({
+ activeIds,
+ items,
+}: {
+ activeIds: readonly string[]
+ items: readonly DocTocItem[]
+}) {
+ const t = useTranslate()
+ const label = t({ zh: "本页内容", us: "On this page" })
+ const rootRef = React.useRef(null)
+ const railRef = React.useRef(null)
+ const [marker, setMarker] = React.useState<{
+ height: number
+ top: number
+ } | null>(null)
+ const activeKey = activeIds.join("\n")
+
+ React.useLayoutEffect(() => {
+ const root = rootRef.current
+ const rail = railRef.current
+ if (!root || !rail) return
+
+ function measureMarker() {
+ const activeIdSet = new Set(activeIds)
+ const activeRows = Array.from(
+ root!.querySelectorAll("[data-toc-id]")
+ ).filter((row) => activeIdSet.has(row.dataset.tocId ?? ""))
+
+ if (activeRows.length === 0 || rail!.offsetHeight === 0) {
+ setMarker(null)
+ return
+ }
+
+ const railRect = rail!.getBoundingClientRect()
+ const firstRect = activeRows[0]!.getBoundingClientRect()
+ const lastRect = activeRows.at(-1)!.getBoundingClientRect()
+ setMarker({
+ top: firstRect.top - railRect.top,
+ height: lastRect.bottom - firstRect.top,
+ })
+ }
+
+ measureMarker()
+ const resizeObserver = new ResizeObserver(measureMarker)
+ resizeObserver.observe(root)
+ window.addEventListener("resize", measureMarker)
+
+ return () => {
+ resizeObserver.disconnect()
+ window.removeEventListener("resize", measureMarker)
+ }
+ }, [activeKey, activeIds])
+
+ return (
+
+ )
+}
+
+function TocItem({
+ activeIds,
+ item,
+}: {
+ activeIds: readonly string[]
+ item: DocTocItem
+}) {
+ const active = activeIds.includes(item.id)
+
+ return (
+
+
+ {item.items && item.items.length > 0 && (
+
+ {item.items.map((child) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+function TocAnchor({
+ active,
+ href,
+ itemId,
+ title,
+}: {
+ active: boolean
+ href: string
+ itemId: string
+ title: string
+}) {
+ return (
+
+ )
+}
+
+type TocVisibilityEntry = {
+ id: string
+ parentIds: readonly string[]
+}
+
+function useVisibleTocIds(items: readonly DocTocItem[]) {
+ const entries = React.useMemo(() => flattenTocItems(items), [items])
+ const [activeIds, setActiveIds] = React.useState([])
+
+ React.useEffect(() => {
+ if (entries.length === 0) {
+ setActiveIds([])
+ return
+ }
+
+ let animationFrame: number | undefined
+ const articleElement = document.querySelector(
+ "[data-article-content]"
+ )
+ if (!articleElement) return
+ const article = articleElement
+
+ function update() {
+ animationFrame = undefined
+ const headings = entries.flatMap((entry) => {
+ const element = document.getElementById(entry.id)
+ return element ? [{ element, entry }] : []
+ })
+ const viewportTop = getVisibleViewportTop()
+ const viewportBottom = window.innerHeight
+ const articleBottom = article.getBoundingClientRect().bottom
+ const visibleIds = new Set()
+
+ headings.forEach(({ element, entry }, index) => {
+ const sectionTop = element.getBoundingClientRect().top
+ const sectionBottom =
+ headings[index + 1]?.element.getBoundingClientRect().top ??
+ articleBottom
+
+ if (sectionBottom > viewportTop && sectionTop < viewportBottom) {
+ entry.parentIds.forEach((id) => visibleIds.add(id))
+ visibleIds.add(entry.id)
+ }
+ })
+
+ const nextActiveIds = entries
+ .map((entry) => entry.id)
+ .filter((id) => visibleIds.has(id))
+ setActiveIds((current) =>
+ arraysEqual(current, nextActiveIds) ? current : nextActiveIds
+ )
+ }
+
+ function scheduleUpdate() {
+ if (animationFrame !== undefined) return
+ animationFrame = window.requestAnimationFrame(update)
+ }
+
+ scheduleUpdate()
+ const resizeObserver = new ResizeObserver(scheduleUpdate)
+ resizeObserver.observe(article)
+ window.addEventListener("scroll", scheduleUpdate, { passive: true })
+ window.addEventListener("resize", scheduleUpdate)
+
+ return () => {
+ if (animationFrame !== undefined) {
+ window.cancelAnimationFrame(animationFrame)
+ }
+ resizeObserver.disconnect()
+ window.removeEventListener("scroll", scheduleUpdate)
+ window.removeEventListener("resize", scheduleUpdate)
+ }
+ }, [entries])
+
+ return activeIds
+}
+
+function flattenTocItems(
+ items: readonly DocTocItem[],
+ parentIds: readonly string[] = []
+): readonly TocVisibilityEntry[] {
+ return items.flatMap((item) => [
+ { id: item.id, parentIds },
+ ...flattenTocItems(item.items ?? [], [...parentIds, item.id]),
+ ])
+}
+
+function getVisibleViewportTop() {
+ return Array.from(
+ document.querySelectorAll("[data-article-sticky-header]")
+ ).reduce((bottom, element) => {
+ const rect = element.getBoundingClientRect()
+ return rect.top <= 0 && rect.bottom > 0
+ ? Math.max(bottom, rect.bottom)
+ : bottom
+ }, 0)
+}
+
+function arraysEqual(left: readonly string[], right: readonly string[]) {
+ return (
+ left.length === right.length &&
+ left.every((value, index) => value === right[index])
+ )
+}
+
+function Footer({
+ next,
+ previous,
+}: {
+ next?: DocLocation
+ previous?: DocLocation
+}) {
+ return (
+
+ )
+}
+
+function Surround({
+ href,
+ title,
+ description,
+ align = "right",
+}: {
+ title: string
+ description: string
+ align?: "left" | "right"
+ href: string
+}) {
+ return (
+
+
+ {align === "left" ? (
+
+ ) : (
+
+ )}
+
+ {title}
+
+ {description}
+
+
+ )
+}
+
+function ArticleNotFound() {
+ const locale = useLocale()
+ const t = useTranslate()
+
+ return (
+
+
+ {t({ zh: "未找到文档", us: "Document not found" })}
+
+
+ {t({
+ zh: "请求的文档包或页面不存在。",
+ us: "The requested package or document does not exist.",
+ })}
+
+
+ {t({ zh: "打开文档", us: "Open the documentation" })}
+
+
+ )
+}
diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx
new file mode 100644
index 0000000..edf1550
--- /dev/null
+++ b/docs/src/pages/index.tsx
@@ -0,0 +1,49 @@
+import { Actions, Container } from "../components/layout"
+import { getDocHref, getDocPackages } from "../content/registry"
+import { useLocale, useTranslate } from "../lib/locale"
+
+export default function IndexPage() {
+ const locale = useLocale()
+ const t = useTranslate()
+ const docPackages = getDocPackages(locale)
+
+ return (
+ <>
+
+
+ my-shadcn-ui
+
+
+
+
+ {docPackages.map((docPackage, index) => (
+
+
+
+ {index > 9 ? `${index + 1}` : `0${index + 1}`}
+
+
+ {docPackage.label}
+
+
+ {docPackage.description}
+
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/docs/src/pages/settings.tsx b/docs/src/pages/settings.tsx
new file mode 100644
index 0000000..5067137
--- /dev/null
+++ b/docs/src/pages/settings.tsx
@@ -0,0 +1,365 @@
+import { Icon } from "@icones/react"
+import { usePreference } from "@workspace/preferences"
+
+import { BackButton, Container, SlashLine } from "../components/layout"
+import { LocaleFlag } from "../components/locale-flag"
+import { SearchTrigger } from "../components/search-trigger"
+import { Swatch } from "../components/swatch"
+import { getLocalizedHref } from "../content/registry"
+import type { DocLocale } from "../content/types"
+import {
+ accentColors,
+ backgroundShades,
+ getPaletteColor,
+ themeModes,
+ useAppearance,
+ type AccentColor,
+ type BackgroundShade,
+ type ForegroundShade,
+ type ThemeMode,
+} from "../lib/appearance"
+import { useLocale, useTranslate, type Translation } from "../lib/locale"
+import { navigateToLocale } from "../lib/locale-navigation"
+import { detectLocale } from "../lib/preferences"
+
+const foregroundOptions: ReadonlyArray<{
+ label: Translation
+ value: ForegroundShade
+}> = [
+ { value: "slate", label: { zh: "冷色", us: "Cool" } },
+ { value: "neutral", label: { zh: "中性", us: "Neutral" } },
+ { value: "stone", label: { zh: "暖色", us: "Warm" } },
+]
+
+const messages = {
+ accent: { zh: "强调色", us: "Accent color" },
+ appearance: { zh: "外观", us: "Appearance" },
+ autoDetect: { zh: "自动检测", us: "Automatic detection" },
+ autoDetectDescription: {
+ zh: "跟随浏览器或操作系统语言设置。",
+ us: "Follow the browser or operating system language.",
+ },
+ background: { zh: "背景色调", us: "Background shade" },
+ enable: { zh: "启用", us: "Enable" },
+ foreground: { zh: "前景色调", us: "Foreground shade" },
+ language: { zh: "语言", us: "Language" },
+ settings: { zh: "设置", us: "Settings" },
+ theme: { zh: "主题", us: "Theme" },
+ themeModes: {
+ light: { zh: "浅色", us: "Light" },
+ dark: { zh: "深色", us: "Dark" },
+ system: { zh: "跟随系统", us: "System" },
+ },
+} as const
+
+export default function SettingsPage() {
+ const locale = useLocale()
+ const t = useTranslate()
+ const appearance = useAppearance()
+ const [localeMode, setLocaleMode] = usePreference("locale-mode")
+ const [, setPreferredLocale] = usePreference("locale")
+ const autoDetectLocale = localeMode === "auto"
+
+ function selectLocale(nextLocale: DocLocale) {
+ setPreferredLocale(nextLocale)
+ setLocaleMode("manual")
+ navigateToLocale(nextLocale)
+ }
+
+ function setAutomaticLocale(enabled: boolean) {
+ if (enabled) {
+ setLocaleMode("auto")
+ navigateToLocale(detectLocale())
+ } else {
+ setPreferredLocale(locale)
+ setLocaleMode("manual")
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+ {t(messages.settings)}
+
+
+
+
+
+
+
+
+
+
+ {t(messages.appearance)}
+
+
+
+
+
({
+ light: getPaletteColor(accentColor, 600),
+ dark: getPaletteColor(accentColor, 400),
+ })}
+ onChange={(accentColor) =>
+ appearance.updateAppearance({ accentColor })
+ }
+ />
+
+ ({
+ light: getPaletteColor(backgroundShade, 50),
+ dark: getPaletteColor(backgroundShade, 950),
+ })}
+ onChange={(backgroundShade) =>
+ appearance.updateAppearance({ backgroundShade })
+ }
+ />
+
+
+
+
+
+
+
+ {t(messages.language)}
+
+
+
+
+
+
{t(messages.autoDetect)}
+
+
+ {t(messages.autoDetectDescription)}
+
+
+
+
+
+
+ >
+ )
+}
+
+function ColorField({
+ getColors,
+ label,
+ onChange,
+ options,
+ value,
+}: {
+ getColors: (value: T) => { dark: string; light: string }
+ label: string
+ onChange: (value: T) => void
+ options: readonly T[]
+ value: T
+}) {
+ return (
+
+ )
+}
+
+function ThemeIcon({ mode }: { mode: ThemeMode }) {
+ if (mode === "light") {
+ return
+ }
+ if (mode === "dark") {
+ return
+ }
+ return
+}
+
+function LocaleLink({
+ disabled,
+ href,
+ label,
+ locale,
+ onClick,
+ selected,
+}: {
+ disabled: boolean
+ href: string
+ label: string
+ locale: DocLocale
+ onClick: () => void
+ selected: boolean
+}) {
+ return (
+ {
+ if (disabled) {
+ event.preventDefault()
+ return
+ }
+ onClick()
+ }}
+ >
+
+ {label}
+ {selected && }
+
+ )
+}
+
+function capitalize(value: string) {
+ return value.charAt(0).toUpperCase() + value.slice(1)
+}
diff --git a/docs/src/satteri-modules.d.ts b/docs/src/satteri-modules.d.ts
new file mode 100644
index 0000000..f743f48
--- /dev/null
+++ b/docs/src/satteri-modules.d.ts
@@ -0,0 +1,9 @@
+declare module "*.mdx" {
+ import type { ComponentType, ElementType } from "react"
+
+ const MDXContent: ComponentType<{
+ components?: Record
+ }>
+ export const frontmatter: Record
+ export default MDXContent
+}
diff --git a/docs/tsconfig.app.json b/docs/tsconfig.app.json
new file mode 100644
index 0000000..6830b6f
--- /dev/null
+++ b/docs/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/docs/tsconfig.node.json b/docs/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/docs/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/docs/vite.config.ts b/docs/vite.config.ts
new file mode 100644
index 0000000..413f52f
--- /dev/null
+++ b/docs/vite.config.ts
@@ -0,0 +1,81 @@
+import react, { reactCompilerPreset } from "@vitejs/plugin-react"
+import babel from "@rolldown/plugin-babel"
+import tailwindcss from "@tailwindcss/vite"
+import { icones } from "@icones/vite"
+import { readFile } from "node:fs/promises"
+import { fileURLToPath } from "node:url"
+import { defineConfig, type Plugin } from "vite"
+import satteri from "vite-plugin-satteri"
+
+const packagesDirectory = fileURLToPath(new URL("../packages", import.meta.url))
+
+/**
+ * Vite 默认监听 docs 根目录。现有的外部 MDX 会作为模块被监听,但新建 package
+ * 或新建 MDX 文件在首次进入模块图之前不会触发 import.meta.glob 重新计算。
+ */
+function watchPackageDocs(): Plugin {
+ return {
+ name: "watch-package-docs",
+ configureServer(server) {
+ server.watcher.add(packagesDirectory)
+ },
+ }
+}
+
+function loadDocSearchSources(): Plugin {
+ const suffix = "?docs-search-raw"
+ const virtualPrefix = "\0docs-search-raw:"
+
+ return {
+ name: "load-doc-search-sources",
+ enforce: "pre",
+ async resolveId(id, importer) {
+ if (!id.endsWith(suffix)) return
+
+ const sourceId = id.slice(0, -suffix.length)
+ const resolved = await this.resolve(sourceId, importer, {
+ skipSelf: true,
+ })
+ const filename = resolved?.id ?? sourceId
+ return `${virtualPrefix}${encodeURIComponent(filename)}.js`
+ },
+ async load(id) {
+ if (!id.startsWith(virtualPrefix)) return
+
+ const sourceId = decodeURIComponent(
+ id.slice(virtualPrefix.length, -".js".length)
+ )
+ const filename = sourceId.startsWith("/@fs/")
+ ? sourceId.slice("/@fs".length)
+ : sourceId
+ this.addWatchFile(filename)
+ const source = await readFile(filename, "utf8")
+ return `export default ${JSON.stringify(source)}`
+ },
+ }
+}
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ watchPackageDocs(),
+ loadDocSearchSources(),
+ satteri({
+ features: {
+ frontmatter: true,
+ gfm: true,
+ headingAttributes: true,
+ },
+ mdx: {
+ jsxImportSource: "react",
+ },
+ }),
+ tailwindcss(),
+ icones({
+ mode: "symbol",
+ emitData: false,
+ }),
+ react(),
+ babel({ presets: [reactCompilerPreset()] }),
+ ],
+})
diff --git a/package.json b/package.json
index 570937c..8c385ec 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
},
"workspaces": [
"apps/*",
+ "docs",
"packages/*"
]
}
diff --git a/packages/blocks/docs/en-US/advanced/appearance.mdx b/packages/blocks/docs/en-US/advanced/appearance.mdx
new file mode 100644
index 0000000..f174fde
--- /dev/null
+++ b/packages/blocks/docs/en-US/advanced/appearance.mdx
@@ -0,0 +1,40 @@
+---
+title: Appearance
+description: Finish the application with persistent theme, color, and compact-layout preferences.
+order: 22
+toc:
+ - id: provide-ui-state
+ title: Provide UI state
+ - id: apply-preferences
+ title: Apply preferences
+ - id: add-locales
+ title: Add locales
+---
+
+## Provide UI state {#provide-ui-state}
+
+Mount `UiStateProvider` near the application root. A controlled provider can persist every state update through its change handler.
+
+```tsx
+
+
+
+
+
+```
+
+## Apply preferences {#apply-preferences}
+
+Render `AppearanceController` once so the current state becomes document classes and theme variables. `useUiState` reads or updates individual preferences, while `useResolvedTheme` returns the effective light or dark scheme after resolving system mode.
+
+## Add locales {#add-locales}
+
+Add the appearance catalog only when the application uses these controls:
+
+```json
+{
+ "catalogSources": ["@workspace/blocks/appearance/locales/{locale}"]
+}
+```
+
+The application shell is now complete. Add only the other block-specific catalogs used by the product.
diff --git a/packages/blocks/docs/en-US/advanced/media.mdx b/packages/blocks/docs/en-US/advanced/media.mdx
new file mode 100644
index 0000000..2edec21
--- /dev/null
+++ b/packages/blocks/docs/en-US/advanced/media.mdx
@@ -0,0 +1,34 @@
+---
+title: Media library
+description: Supply a storage adapter and add browsing, upload, folders, and asset selection.
+order: 21
+toc:
+ - id: implement-the-adapter
+ title: Implement the adapter
+ - id: provide-media
+ title: Provide media
+ - id: selection-flows
+ title: Selection flows
+---
+
+## Implement the adapter {#implement-the-adapter}
+
+`MediaAdapter` is the media feature's complete persistence boundary. Implement its query, upload, folder, update, and delete operations with the product's storage service.
+
+## Provide media {#provide-media}
+
+Mount the adapter once around the media surfaces:
+
+```tsx
+import { MediaLibrary, MediaProvider } from "@workspace/blocks/media"
+
+;
+
+
+```
+
+The optional `notify` callback translates operational results into the application's toast or notification system.
+
+## Selection flows {#selection-flows}
+
+Use `MediaPickerDialog` for single or multiple asset selection. `readMediaDimensions`, `formatMediaFileSize`, and `defaultMediaReference` cover the common presentation work around uploads and previews.
diff --git a/packages/blocks/docs/en-US/advanced/notifications.mdx b/packages/blocks/docs/en-US/advanced/notifications.mdx
new file mode 100644
index 0000000..32b86cb
--- /dev/null
+++ b/packages/blocks/docs/en-US/advanced/notifications.mdx
@@ -0,0 +1,21 @@
+---
+title: Notifications
+description: Connect notification state without coupling the UI to a backend.
+order: 20
+toc:
+ - id: notifications-query
+ title: Notifications query
+---
+
+## Notifications query {#notifications-query}
+
+Notifications use TanStack Query as their data boundary:
+
+```tsx
+const notifications = useNotifications({
+ queryKey: ["notifications"],
+ queryFn: loadNotifications,
+})
+```
+
+The result includes unread counts, optimistic read-state updates, action execution, refetching, and pending or error status. `AppLayout` can create the sheet directly from the same query options.
diff --git a/packages/blocks/docs/en-US/examples/chats.mdx b/packages/blocks/docs/en-US/examples/chats.mdx
new file mode 100644
index 0000000..38b75df
--- /dev/null
+++ b/packages/blocks/docs/en-US/examples/chats.mdx
@@ -0,0 +1,35 @@
+---
+title: Chats
+description: Add a responsive conversation workspace and connect its events to application services.
+order: 30
+toc:
+ - id: render-the-workspace
+ title: Render the workspace
+ - id: controlled-state
+ title: Controlled state
+ - id: media-and-pagination
+ title: Media and pagination
+---
+
+## Render the workspace {#render-the-workspace}
+
+`ChatWorkspace` renders conversation discovery, the active thread, and the composer:
+
+```tsx
+import { ChatWorkspace } from "@workspace/blocks/chats"
+
+; sendMessage(conversation, message)}
+/>
+```
+
+## Controlled state {#controlled-state}
+
+The active conversation, draft, search query, and Enter-to-send preference can be controlled individually. Leave a value undefined when the workspace should own that state.
+
+The UI emits events but does not select a transport or persistence format. Map API responses into `ChatConversation` and the exported message variants at the application boundary.
+
+## Media and pagination {#media-and-pagination}
+
+Provide `resolveMediaUrl` when stored media references need signed or transformed URLs. Use `onLoadEarlierMessages` with `hasMoreMessages` and `isLoadingMoreMessages` to connect historical pagination.
diff --git a/packages/blocks/docs/en-US/guide/application-shell.mdx b/packages/blocks/docs/en-US/guide/application-shell.mdx
new file mode 100644
index 0000000..a52f69f
--- /dev/null
+++ b/packages/blocks/docs/en-US/guide/application-shell.mdx
@@ -0,0 +1,45 @@
+---
+title: Application shell
+description: Assemble the shared header, sidebar, breadcrumbs, chat, and notification surfaces.
+order: 11
+toc:
+ - id: render-app-layout
+ title: Render AppLayout
+ - id: layout-inputs
+ title: Layout inputs
+ - id: optional-preset
+ title: Optional preset
+---
+
+## Render AppLayout {#render-app-layout}
+
+`AppLayout` is the integration point for the rest of this tutorial:
+
+```tsx
+import { AppLayout } from "@workspace/blocks/layout"
+
+export function WorkspaceLayout() {
+ return (
+
+
+
+ )
+}
+```
+
+## Layout inputs {#layout-inputs}
+
+The shell composes `AppSidebar`, `AppBreadcrumb`, `AppQueryIndicator`, `UserMenu`, a notification sheet, and chat threads. Pass `headerActions` when the product needs additional global controls.
+
+The route content remains the layout's `children`, so the package does not constrain the router used by the application.
+
+## Optional preset {#optional-preset}
+
+`@workspace/blocks/layouts/vega` provides an optional visual shell. It lives on a separate entry point so the standard layout does not include preset-specific code.
diff --git a/packages/blocks/docs/en-US/guide/installation.mdx b/packages/blocks/docs/en-US/guide/installation.mdx
new file mode 100644
index 0000000..218fadc
--- /dev/null
+++ b/packages/blocks/docs/en-US/guide/installation.mdx
@@ -0,0 +1,40 @@
+---
+title: Installation
+description: Add the package, its styles, and the providers required by the tutorial.
+order: 10
+toc:
+ - id: add-the-package
+ title: Add the package
+ - id: import-styles
+ title: Import styles
+ - id: application-providers
+ title: Application providers
+---
+
+## Add the package {#add-the-package}
+
+Add the workspace dependency to the consuming application:
+
+```json
+{
+ "dependencies": {
+ "@workspace/blocks": "workspace:*"
+ }
+}
+```
+
+Blocks use `@workspace/ui`, React Query, and the workspace internationalization runtime. The workspace package manager resolves those dependencies for local applications.
+
+## Import styles {#import-styles}
+
+Import the block stylesheet once from the application entry:
+
+```ts
+import "@workspace/blocks/globals.css"
+```
+
+Keep feature imports on their explicit subpaths. For example, importing `@workspace/blocks/media` does not pull chat into the same module graph.
+
+## Application providers {#application-providers}
+
+The full shell expects React Query and internationalization to be available above it. Add those providers before the route tree, then continue to the application-shell chapter.
diff --git a/packages/blocks/docs/en-US/guide/navigation.mdx b/packages/blocks/docs/en-US/guide/navigation.mdx
new file mode 100644
index 0000000..f5d3b97
--- /dev/null
+++ b/packages/blocks/docs/en-US/guide/navigation.mdx
@@ -0,0 +1,46 @@
+---
+title: Navigation
+description: Define navigation groups once and connect them to desktop and mobile surfaces.
+order: 12
+toc:
+ - id: define-groups
+ title: Define groups
+ - id: provide-navigation
+ title: Provide navigation
+ - id: route-state
+ title: Route state
+---
+
+## Define groups {#define-groups}
+
+Start with typed `NavigationGroup` values. Items may represent direct routes or nested navigation branches.
+
+```ts
+import type { NavigationGroup } from "@workspace/blocks/navigation"
+
+export const navigationGroups: NavigationGroup[] = [
+ {
+ id: "workspace",
+ label: "Workspace",
+ items: [
+ { id: "dashboard", label: "Dashboard", to: "/dashboard" },
+ { id: "media", label: "Media", to: "/media" },
+ ],
+ },
+]
+```
+
+## Provide navigation {#provide-navigation}
+
+`AppLayout` already mounts the provider and the responsive navigation surfaces. For a custom shell, compose them directly:
+
+```tsx
+
+
+
+
+```
+
+## Route state {#route-state}
+
+Use `resolveNavigationRouteState` for the standard route model. Applications with custom routing can supply a `GetNavigationRouteState` implementation while keeping the same navigation UI.
diff --git a/packages/blocks/docs/en-US/overview.mdx b/packages/blocks/docs/en-US/overview.mdx
new file mode 100644
index 0000000..d35320d
--- /dev/null
+++ b/packages/blocks/docs/en-US/overview.mdx
@@ -0,0 +1,33 @@
+---
+title: Overview
+description: Understand how the Blocks package turns UI primitives into complete application features.
+order: 1
+toc:
+ - id: what-you-will-build
+ title: What you will build
+ - id: design-boundary
+ title: Design boundary
+ - id: tutorial-map
+ title: Tutorial map
+---
+
+## What you will build {#what-you-will-build}
+
+This tutorial builds an application shell step by step. You will begin with the shared layout, connect navigation, then add notifications, chat, media, and appearance preferences.
+
+By the end, the application owns its data and business rules while `@workspace/blocks` owns the reusable presentation and interaction patterns.
+
+## Design boundary {#design-boundary}
+
+Blocks sit above `@workspace/ui`. They combine low-level components into features, but they do not choose your API, database, router configuration, or storage provider.
+
+> Keep data access in the host application. Pass it into a block through props, query functions, providers, or adapters.
+
+## Tutorial map {#tutorial-map}
+
+1. Install the package and global styles.
+2. Create the application shell.
+3. Define navigation and route state.
+4. Connect notifications.
+5. Add chat and media workflows.
+6. Finish with persistent appearance controls.
diff --git a/packages/blocks/docs/zh-Hans/advanced/appearance.mdx b/packages/blocks/docs/zh-Hans/advanced/appearance.mdx
new file mode 100644
index 0000000..a50dae8
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/advanced/appearance.mdx
@@ -0,0 +1,40 @@
+---
+title: 外观设置
+description: 使用可持久化的主题、颜色和紧凑布局偏好完成应用。
+order: 22
+toc:
+ - id: provide-ui-state
+ title: 提供 UI 状态
+ - id: apply-preferences
+ title: 应用偏好
+ - id: add-locales
+ title: 添加词典
+---
+
+## 提供 UI 状态 {#provide-ui-state}
+
+在应用根节点附近挂载 `UiStateProvider`。受控 Provider 可以通过变更处理器持久化每次状态更新。
+
+```tsx
+
+
+
+
+
+```
+
+## 应用偏好 {#apply-preferences}
+
+渲染一次 `AppearanceController`,使当前状态转换为文档类名和主题变量。`useUiState` 读取或更新单个偏好,`useResolvedTheme` 在解析系统模式后返回最终的亮色或暗色主题。
+
+## 添加词典 {#add-locales}
+
+只有应用使用这些控件时才添加外观词典:
+
+```json
+{
+ "catalogSources": ["@workspace/blocks/appearance/locales/{locale}"]
+}
+```
+
+至此应用外壳已经完成。其他功能也只需添加产品实际使用的 Block 词典。
diff --git a/packages/blocks/docs/zh-Hans/advanced/media.mdx b/packages/blocks/docs/zh-Hans/advanced/media.mdx
new file mode 100644
index 0000000..f1e46f1
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/advanced/media.mdx
@@ -0,0 +1,34 @@
+---
+title: 媒体库
+description: 提供存储 Adapter,并加入浏览、上传、文件夹和资源选择能力。
+order: 21
+toc:
+ - id: implement-the-adapter
+ title: 实现 Adapter
+ - id: provide-media
+ title: 提供媒体能力
+ - id: selection-flows
+ title: 选择流程
+---
+
+## 实现 Adapter {#implement-the-adapter}
+
+`MediaAdapter` 是媒体功能完整的持久化边界。通过产品存储服务实现查询、上传、文件夹、更新和删除操作。
+
+## 提供媒体能力 {#provide-media}
+
+在媒体界面外挂载一次 Adapter:
+
+```tsx
+import { MediaLibrary, MediaProvider } from "@workspace/blocks/media"
+
+;
+
+
+```
+
+可选的 `notify` 回调把操作结果转换为应用的 Toast 或通知。
+
+## 选择流程 {#selection-flows}
+
+单选或多选资源使用 `MediaPickerDialog`。`readMediaDimensions`、`formatMediaFileSize` 和 `defaultMediaReference` 覆盖上传与预览相关的常用展示工作。
diff --git a/packages/blocks/docs/zh-Hans/advanced/notifications.mdx b/packages/blocks/docs/zh-Hans/advanced/notifications.mdx
new file mode 100644
index 0000000..9848680
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/advanced/notifications.mdx
@@ -0,0 +1,21 @@
+---
+title: 通知
+description: 接入通知状态,同时避免让 UI 与后端耦合。
+order: 20
+toc:
+ - id: notifications-query
+ title: 通知查询
+---
+
+## 通知查询 {#notifications-query}
+
+通知以 TanStack Query 作为数据边界:
+
+```tsx
+const notifications = useNotifications({
+ queryKey: ["notifications"],
+ queryFn: loadNotifications,
+})
+```
+
+返回结果包含未读数量、乐观已读更新、操作执行、重新请求以及等待和错误状态。`AppLayout` 可以直接通过相同查询选项创建通知 Sheet。
diff --git a/packages/blocks/docs/zh-Hans/examples/chats.mdx b/packages/blocks/docs/zh-Hans/examples/chats.mdx
new file mode 100644
index 0000000..700ce07
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/examples/chats.mdx
@@ -0,0 +1,35 @@
+---
+title: 聊天示例
+description: 添加响应式会话工作区,并将事件连接到应用服务。
+order: 30
+toc:
+ - id: render-the-workspace
+ title: 渲染工作区
+ - id: controlled-state
+ title: 受控状态
+ - id: media-and-pagination
+ title: 媒体与分页
+---
+
+## 渲染工作区 {#render-the-workspace}
+
+`ChatWorkspace` 渲染会话查找、当前线程和消息编辑器:
+
+```tsx
+import { ChatWorkspace } from "@workspace/blocks/chats"
+
+; sendMessage(conversation, message)}
+/>
+```
+
+## 受控状态 {#controlled-state}
+
+当前会话、草稿、搜索词和回车发送偏好都可以单独受控。当工作区应自行管理某项状态时,不传对应值即可。
+
+UI 只发送事件,不选择传输方式或持久化格式。在应用边界将 API 响应转换为 `ChatConversation` 和导出的消息类型。
+
+## 媒体与分页 {#media-and-pagination}
+
+当已存储媒体需要签名或转换 URL 时,提供 `resolveMediaUrl`。使用 `onLoadEarlierMessages`、`hasMoreMessages` 和 `isLoadingMoreMessages` 接入历史消息分页。
diff --git a/packages/blocks/docs/zh-Hans/guide/application-shell.mdx b/packages/blocks/docs/zh-Hans/guide/application-shell.mdx
new file mode 100644
index 0000000..371adfd
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/guide/application-shell.mdx
@@ -0,0 +1,45 @@
+---
+title: 应用外壳
+description: 组装共享 Header、Sidebar、面包屑、聊天和通知界面。
+order: 11
+toc:
+ - id: render-app-layout
+ title: 渲染 AppLayout
+ - id: layout-inputs
+ title: 布局输入
+ - id: optional-preset
+ title: 可选预设
+---
+
+## 渲染 AppLayout {#render-app-layout}
+
+`AppLayout` 是本教程其余部分的集成点:
+
+```tsx
+import { AppLayout } from "@workspace/blocks/layout"
+
+export function WorkspaceLayout() {
+ return (
+
+
+
+ )
+}
+```
+
+## 布局输入 {#layout-inputs}
+
+应用外壳组合了 `AppSidebar`、`AppBreadcrumb`、`AppQueryIndicator`、`UserMenu`、通知 Sheet 和聊天线程。产品需要更多全局控件时可传入 `headerActions`。
+
+路由内容仍作为布局的 `children`,因此该包不会限制应用使用的路由器。
+
+## 可选预设 {#optional-preset}
+
+`@workspace/blocks/layouts/vega` 提供可选的视觉外壳。它位于独立入口,因此标准布局不会包含预设专属代码。
diff --git a/packages/blocks/docs/zh-Hans/guide/installation.mdx b/packages/blocks/docs/zh-Hans/guide/installation.mdx
new file mode 100644
index 0000000..37c836e
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/guide/installation.mdx
@@ -0,0 +1,40 @@
+---
+title: 安装
+description: 添加教程所需的包、样式和 Providers。
+order: 10
+toc:
+ - id: add-the-package
+ title: 添加包
+ - id: import-styles
+ title: 导入样式
+ - id: application-providers
+ title: 应用 Providers
+---
+
+## 添加包 {#add-the-package}
+
+在使用方应用中添加工作区依赖:
+
+```json
+{
+ "dependencies": {
+ "@workspace/blocks": "workspace:*"
+ }
+}
+```
+
+Blocks 使用 `@workspace/ui`、React Query 和工作区国际化运行时。工作区包管理器会为本地应用解析这些依赖。
+
+## 导入样式 {#import-styles}
+
+在应用入口导入一次 Block 样式:
+
+```ts
+import "@workspace/blocks/globals.css"
+```
+
+功能模块应始终从显式子路径导入。例如,导入 `@workspace/blocks/media` 不会把聊天功能加入同一个模块图。
+
+## 应用 Providers {#application-providers}
+
+完整应用外壳要求上层已经提供 React Query 和国际化。在路由树之前加入这些 Providers,然后继续应用外壳章节。
diff --git a/packages/blocks/docs/zh-Hans/guide/navigation.mdx b/packages/blocks/docs/zh-Hans/guide/navigation.mdx
new file mode 100644
index 0000000..41614af
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/guide/navigation.mdx
@@ -0,0 +1,46 @@
+---
+title: 导航
+description: 统一定义导航分组,并将其连接到桌面和移动端界面。
+order: 12
+toc:
+ - id: define-groups
+ title: 定义分组
+ - id: provide-navigation
+ title: 提供导航
+ - id: route-state
+ title: 路由状态
+---
+
+## 定义分组 {#define-groups}
+
+从类型安全的 `NavigationGroup` 开始。导航项可以是直接路由,也可以是嵌套导航分支。
+
+```ts
+import type { NavigationGroup } from "@workspace/blocks/navigation"
+
+export const navigationGroups: NavigationGroup[] = [
+ {
+ id: "workspace",
+ label: "工作区",
+ items: [
+ { id: "dashboard", label: "仪表盘", to: "/dashboard" },
+ { id: "media", label: "媒体", to: "/media" },
+ ],
+ },
+]
+```
+
+## 提供导航 {#provide-navigation}
+
+`AppLayout` 已经挂载 Provider 和响应式导航界面。自定义外壳可以直接组合它们:
+
+```tsx
+
+
+
+
+```
+
+## 路由状态 {#route-state}
+
+标准路由模型使用 `resolveNavigationRouteState`。使用自定义路由的应用可以提供 `GetNavigationRouteState` 实现,同时复用相同导航 UI。
diff --git a/packages/blocks/docs/zh-Hans/overview.mdx b/packages/blocks/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..d29c561
--- /dev/null
+++ b/packages/blocks/docs/zh-Hans/overview.mdx
@@ -0,0 +1,33 @@
+---
+title: 概览
+description: 了解 Blocks 包如何把 UI 基础组件组合成完整应用功能。
+order: 1
+toc:
+ - id: what-you-will-build
+ title: 将要构建的内容
+ - id: design-boundary
+ title: 设计边界
+ - id: tutorial-map
+ title: 教程路线
+---
+
+## 将要构建的内容 {#what-you-will-build}
+
+本教程会逐步构建一个应用外壳。你将从共享布局开始,接入导航,再添加通知、聊天、媒体和外观偏好。
+
+完成后,应用负责数据和业务规则,`@workspace/blocks` 负责可复用的展示与交互模式。
+
+## 设计边界 {#design-boundary}
+
+Blocks 位于 `@workspace/ui` 之上。它们将底层组件组合成完整功能,但不会替你选择 API、数据库、路由配置或存储服务。
+
+> 将数据访问保留在宿主应用中,通过属性、查询函数、Provider 或 Adapter 传给 Block。
+
+## 教程路线 {#tutorial-map}
+
+1. 安装包和全局样式。
+2. 创建应用外壳。
+3. 定义导航和路由状态。
+4. 接入通知。
+5. 添加聊天和媒体工作流。
+6. 使用持久化外观控件完成应用。
diff --git a/packages/i18n/docs/en-US/advanced/catalogs.mdx b/packages/i18n/docs/en-US/advanced/catalogs.mdx
new file mode 100644
index 0000000..8f481ec
--- /dev/null
+++ b/packages/i18n/docs/en-US/advanced/catalogs.mdx
@@ -0,0 +1,33 @@
+---
+title: Production catalogs
+description: Understand merging, compact message keys, asset generation, and SSR behavior.
+order: 20
+toc:
+ - id: merge-order
+ title: Merge order
+ - id: build-output
+ title: Build output
+ - id: server-rendering
+ title: Server rendering
+---
+
+## Merge order {#merge-order}
+
+Package sources are merged in configured order, followed by the application catalog. An application therefore translates only its own messages unless it intentionally customizes package copy.
+
+> Import only the locale sources used by the application. Block-specific entries do not import catalogs from unrelated features.
+
+## Build output {#build-output}
+
+During production builds, the plugin:
+
+1. merges configured catalogs for every locale;
+2. creates one shared schema from semantic message IDs;
+3. replaces those IDs with deterministic compact keys;
+4. emits one content-hashed JSON asset per locale.
+
+The private schema is diagnostic build state and is not shipped to the browser.
+
+## Server rendering {#server-rendering}
+
+The SSR bundle embeds the matching compact catalog. Browser navigation loads only the selected locale asset, so adding languages does not duplicate every translation in the main application bundle.
diff --git a/packages/i18n/docs/en-US/advanced/cli.mdx b/packages/i18n/docs/en-US/advanced/cli.mdx
new file mode 100644
index 0000000..9545baf
--- /dev/null
+++ b/packages/i18n/docs/en-US/advanced/cli.mdx
@@ -0,0 +1,44 @@
+---
+title: CLI and Message Studio
+description: Add locales, extract messages, compile catalogs, and run the translation interface.
+order: 21
+toc:
+ - id: scripts
+ title: Scripts
+ - id: commands
+ title: Commands
+ - id: daily-workflow
+ title: Daily workflow
+---
+
+## Scripts {#scripts}
+
+```json
+{
+ "scripts": {
+ "i18n": "workspace-i18n",
+ "i18n:extract": "workspace-i18n extract",
+ "i18n:compile": "workspace-i18n compile",
+ "i18n:ui": "workspace-i18n ui"
+ }
+}
+```
+
+## Commands {#commands}
+
+- `new ` validates a BCP 47 tag, updates configuration, and creates the catalog.
+- `extract` finds application messages and updates catalogs.
+- `compile` produces TypeScript catalog modules.
+- `ui` starts Message Studio with Extract and Compile actions.
+
+Every command accepts `--project ` when it is invoked outside the application directory.
+
+## Daily workflow {#daily-workflow}
+
+```sh
+bun run i18n extract
+bun run i18n ui
+bun run i18n compile
+```
+
+Commit the application configuration and the `en-US` and `zh-Hans` catalogs. Generated private schema data remains build output.
diff --git a/packages/i18n/docs/en-US/examples/devtool.mdx b/packages/i18n/docs/en-US/examples/devtool.mdx
new file mode 100644
index 0000000..f0780e6
--- /dev/null
+++ b/packages/i18n/docs/en-US/examples/devtool.mdx
@@ -0,0 +1,36 @@
+---
+title: Devtool
+description: Inspect and edit catalogs from a floating development interface.
+order: 30
+toc:
+ - id: mount-the-devtool
+ title: Mount the Devtool
+ - id: theme-and-language
+ title: Theme and language
+ - id: custom-surfaces
+ title: Custom surfaces
+---
+
+## Mount the Devtool {#mount-the-devtool}
+
+Mount the Devtool inside `I18nProvider` and only during development:
+
+```tsx
+import { I18nDevtool } from "@workspace/i18n/devtool"
+
+{
+ import.meta.env.DEV && (
+
+ )
+}
+```
+
+The component renders through a portal, so application overflow and stacking contexts do not clip it.
+
+## Theme and language {#theme-and-language}
+
+The `dark` option accepts a class name or a CSS selector. Control-panel language is independent from the translated application locale, supports `en-US` and `zh-Hans`, and falls back from `navigator.languages` to `en-US`.
+
+## Custom surfaces {#custom-surfaces}
+
+Use `MessagePanel`, `MessageRepositoryProvider`, and `createHttpMessageRepository` when the application needs a custom development interface instead of the default floating panel.
diff --git a/packages/i18n/docs/en-US/guide/runtime.mdx b/packages/i18n/docs/en-US/guide/runtime.mdx
new file mode 100644
index 0000000..cfa8de9
--- /dev/null
+++ b/packages/i18n/docs/en-US/guide/runtime.mdx
@@ -0,0 +1,44 @@
+---
+title: Runtime
+description: Load a locale catalog and consume messages through React components and hooks.
+order: 11
+toc:
+ - id: load-the-catalog
+ title: Load the catalog
+ - id: provide-the-locale
+ title: Provide the locale
+ - id: translate-content
+ title: Translate content
+---
+
+## Load the catalog {#load-the-catalog}
+
+The generated catalog loader works in development, production, and SSR builds:
+
+```tsx
+import { use } from "react"
+import { loadMessageCatalog } from "@workspace/i18n/catalogs"
+
+const messages = use(loadMessageCatalog(locale))
+```
+
+## Provide the locale {#provide-the-locale}
+
+```tsx
+
+ {children}
+
+```
+
+## Translate content {#translate-content}
+
+Use `Translate` for JSX content, `useTranslate` for an imperative translation function, and `useMessage` for a single descriptor. `useLocale`, `useLocales`, and `useFormatters` expose locale state and `Intl` formatters.
+
+`Translate` delegates to Lingui without adding a wrapper DOM element.
diff --git a/packages/i18n/docs/en-US/guide/setup.mdx b/packages/i18n/docs/en-US/guide/setup.mdx
new file mode 100644
index 0000000..5676dd8
--- /dev/null
+++ b/packages/i18n/docs/en-US/guide/setup.mdx
@@ -0,0 +1,48 @@
+---
+title: Project setup
+description: Create the application configuration and mount the Vite integration.
+order: 10
+toc:
+ - id: configuration
+ title: Configuration
+ - id: catalog-sources
+ title: Catalog sources
+ - id: vite-plugin
+ title: Vite plugin
+---
+
+## Configuration {#configuration}
+
+Create `i18n.config.json` in the consuming application:
+
+```json
+{
+ "sourceLocale": "en-US",
+ "locales": ["en-US", "zh-Hans"],
+ "catalogPath": "src/locales/{locale}/messages",
+ "catalogSources": ["@workspace/ui/locales/{locale}"],
+ "include": ["src", "../../packages/ui/src"],
+ "exclude": ["**/*.test.{ts,tsx}"]
+}
+```
+
+This file is the application's single persistent internationalization configuration. CLI commands create temporary Lingui configuration only for the child process.
+
+The workspace ships exactly two locales: American English (`en-US`) and Simplified Chinese (`zh-Hans`). Use those same locale keys for application catalogs and package catalog sources.
+
+## Catalog sources {#catalog-sources}
+
+Sources are merged from left to right, and the application catalog is always last. Put reusable package catalogs first so product-specific translations can override them.
+
+## Vite plugin {#vite-plugin}
+
+```ts
+import { i18n } from "@workspace/i18n/vite"
+import { defineConfig } from "vite"
+
+export default defineConfig({
+ plugins: [i18n()],
+})
+```
+
+The plugin serves development catalog APIs and exposes the generated loader through `@workspace/i18n/catalogs`.
diff --git a/packages/i18n/docs/en-US/overview.mdx b/packages/i18n/docs/en-US/overview.mdx
new file mode 100644
index 0000000..fa7487c
--- /dev/null
+++ b/packages/i18n/docs/en-US/overview.mdx
@@ -0,0 +1,35 @@
+---
+title: Overview
+description: Follow the complete path from application configuration to translated production catalogs.
+order: 1
+toc:
+ - id: ownership-model
+ title: Ownership model
+ - id: workflow
+ title: Workflow
+ - id: package-entries
+ title: Package entries
+---
+
+## Ownership model {#ownership-model}
+
+Every application owns its `i18n.config.json` and application catalogs. Reusable packages may ship built-in catalogs, but the application chooses which ones to merge and may override their messages.
+
+This keeps product copy under application control without duplicating translations from shared packages.
+
+## Workflow {#workflow}
+
+1. Configure locales and catalog sources.
+2. Install the Vite plugin.
+3. Load one catalog into `I18nProvider`.
+4. Extract and translate application messages.
+5. Use the Devtool or Message Studio during development.
+6. Compile compact, cacheable production assets.
+
+## Package entries {#package-entries}
+
+- `@workspace/i18n` provides the React runtime.
+- `@workspace/i18n/catalogs` loads generated catalogs.
+- `@workspace/i18n/devtool` provides development surfaces.
+- `@workspace/i18n/vite` configures Vite.
+- `workspace-i18n` exposes the CLI.
diff --git a/packages/i18n/docs/zh-Hans/advanced/catalogs.mdx b/packages/i18n/docs/zh-Hans/advanced/catalogs.mdx
new file mode 100644
index 0000000..a286f89
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/advanced/catalogs.mdx
@@ -0,0 +1,33 @@
+---
+title: 生产环境词典
+description: 理解词典合并、紧凑消息键、资源生成和 SSR 行为。
+order: 20
+toc:
+ - id: merge-order
+ title: 合并顺序
+ - id: build-output
+ title: 构建产物
+ - id: server-rendering
+ title: 服务端渲染
+---
+
+## 合并顺序 {#merge-order}
+
+包词典按配置顺序合并,应用词典最后合并。因此应用默认只需翻译自身消息,除非它有意自定义包内文案。
+
+> 只导入应用实际使用的词典来源。特定 Block 的入口不会导入无关功能的词典。
+
+## 构建产物 {#build-output}
+
+生产构建期间,插件会:
+
+1. 为每种语言合并配置的词典;
+2. 根据语义消息 ID 创建共享 Schema;
+3. 将这些 ID 替换为确定的紧凑键;
+4. 为每种语言输出一个带内容哈希的 JSON 资源。
+
+私有 Schema 仅用于构建诊断,不会发送到浏览器。
+
+## 服务端渲染 {#server-rendering}
+
+SSR Bundle 会嵌入匹配的紧凑词典。浏览器导航只加载选中语言的资源,因此增加语言不会让主应用 Bundle 重复包含所有翻译。
diff --git a/packages/i18n/docs/zh-Hans/advanced/cli.mdx b/packages/i18n/docs/zh-Hans/advanced/cli.mdx
new file mode 100644
index 0000000..1022a67
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/advanced/cli.mdx
@@ -0,0 +1,44 @@
+---
+title: CLI 与 Message Studio
+description: 添加语言、提取消息、编译词典并运行翻译界面。
+order: 21
+toc:
+ - id: scripts
+ title: Scripts
+ - id: commands
+ title: 命令
+ - id: daily-workflow
+ title: 日常工作流
+---
+
+## Scripts {#scripts}
+
+```json
+{
+ "scripts": {
+ "i18n": "workspace-i18n",
+ "i18n:extract": "workspace-i18n extract",
+ "i18n:compile": "workspace-i18n compile",
+ "i18n:ui": "workspace-i18n ui"
+ }
+}
+```
+
+## 命令 {#commands}
+
+- `new ` 校验 BCP 47 标签、更新配置并创建词典。
+- `extract` 查找应用消息并更新词典。
+- `compile` 生成 TypeScript 词典模块。
+- `ui` 启动带有 Extract 和 Compile 操作的 Message Studio。
+
+在应用目录外执行时,每个命令都接受 `--project `。
+
+## 日常工作流 {#daily-workflow}
+
+```sh
+bun run i18n extract
+bun run i18n ui
+bun run i18n compile
+```
+
+提交应用配置以及 `en-US`、`zh-Hans` 词典。生成的私有 Schema 数据仍属于构建产物。
diff --git a/packages/i18n/docs/zh-Hans/examples/devtool.mdx b/packages/i18n/docs/zh-Hans/examples/devtool.mdx
new file mode 100644
index 0000000..5f171cc
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/examples/devtool.mdx
@@ -0,0 +1,36 @@
+---
+title: Devtool
+description: 通过浮动开发界面检查和编辑词典。
+order: 30
+toc:
+ - id: mount-the-devtool
+ title: 挂载 Devtool
+ - id: theme-and-language
+ title: 主题与语言
+ - id: custom-surfaces
+ title: 自定义界面
+---
+
+## 挂载 Devtool {#mount-the-devtool}
+
+将 Devtool 挂载在 `I18nProvider` 内,并且只在开发环境启用:
+
+```tsx
+import { I18nDevtool } from "@workspace/i18n/devtool"
+
+{
+ import.meta.env.DEV && (
+
+ )
+}
+```
+
+组件通过 Portal 渲染,因此不会被应用的 overflow 或层叠上下文裁剪。
+
+## 主题与语言 {#theme-and-language}
+
+`dark` 选项接受类名或 CSS 选择器。控制面板语言独立于被翻译的应用语言,支持 `en-US` 和 `zh-Hans`,并会从 `navigator.languages` 回退到 `en-US`。
+
+## 自定义界面 {#custom-surfaces}
+
+当应用需要替换默认浮动面板时,可使用 `MessagePanel`、`MessageRepositoryProvider` 和 `createHttpMessageRepository` 构建自定义开发界面。
diff --git a/packages/i18n/docs/zh-Hans/guide/runtime.mdx b/packages/i18n/docs/zh-Hans/guide/runtime.mdx
new file mode 100644
index 0000000..6d94ef0
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/guide/runtime.mdx
@@ -0,0 +1,44 @@
+---
+title: 运行时
+description: 加载语言词典,并通过 React 组件和 Hooks 使用消息。
+order: 11
+toc:
+ - id: load-the-catalog
+ title: 加载词典
+ - id: provide-the-locale
+ title: 提供语言
+ - id: translate-content
+ title: 翻译内容
+---
+
+## 加载词典 {#load-the-catalog}
+
+生成的词典加载器同时支持开发、生产和 SSR 构建:
+
+```tsx
+import { use } from "react"
+import { loadMessageCatalog } from "@workspace/i18n/catalogs"
+
+const messages = use(loadMessageCatalog(locale))
+```
+
+## 提供语言 {#provide-the-locale}
+
+```tsx
+
+ {children}
+
+```
+
+## 翻译内容 {#translate-content}
+
+JSX 内容使用 `Translate`,命令式翻译函数使用 `useTranslate`,单个描述符使用 `useMessage`。`useLocale`、`useLocales` 和 `useFormatters` 提供语言状态与 `Intl` 格式化器。
+
+`Translate` 会直接委托给 Lingui,不会增加额外的 DOM 包装元素。
diff --git a/packages/i18n/docs/zh-Hans/guide/setup.mdx b/packages/i18n/docs/zh-Hans/guide/setup.mdx
new file mode 100644
index 0000000..b1f647e
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/guide/setup.mdx
@@ -0,0 +1,48 @@
+---
+title: 项目配置
+description: 创建应用配置并接入 Vite 插件。
+order: 10
+toc:
+ - id: configuration
+ title: 配置文件
+ - id: catalog-sources
+ title: 词典来源
+ - id: vite-plugin
+ title: Vite 插件
+---
+
+## 配置文件 {#configuration}
+
+在使用方应用中创建 `i18n.config.json`:
+
+```json
+{
+ "sourceLocale": "en-US",
+ "locales": ["en-US", "zh-Hans"],
+ "catalogPath": "src/locales/{locale}/messages",
+ "catalogSources": ["@workspace/ui/locales/{locale}"],
+ "include": ["src", "../../packages/ui/src"],
+ "exclude": ["**/*.test.{ts,tsx}"]
+}
+```
+
+该文件是应用唯一的持久国际化配置。CLI 命令只为子进程创建临时 Lingui 配置。
+
+工作区只提供简体中文(`zh-Hans`)和美式英语(`en-US`)。应用词典和包词典来源应使用相同的语言键。
+
+## 词典来源 {#catalog-sources}
+
+词典来源按从左到右的顺序合并,应用词典始终位于最后。将可复用包词典放在前面,使产品特有翻译可以覆盖它们。
+
+## Vite 插件 {#vite-plugin}
+
+```ts
+import { i18n } from "@workspace/i18n/vite"
+import { defineConfig } from "vite"
+
+export default defineConfig({
+ plugins: [i18n()],
+})
+```
+
+插件会提供开发词典 API,并通过 `@workspace/i18n/catalogs` 暴露生成的加载器。
diff --git a/packages/i18n/docs/zh-Hans/overview.mdx b/packages/i18n/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..7f1a191
--- /dev/null
+++ b/packages/i18n/docs/zh-Hans/overview.mdx
@@ -0,0 +1,35 @@
+---
+title: 概览
+description: 了解从应用配置到生产环境翻译词典的完整流程。
+order: 1
+toc:
+ - id: ownership-model
+ title: 所有权模型
+ - id: workflow
+ title: 工作流
+ - id: package-entries
+ title: 包入口
+---
+
+## 所有权模型 {#ownership-model}
+
+每个应用都拥有自己的 `i18n.config.json` 和应用词典。可复用包可以附带内置词典,但由应用决定合并哪些词典,也可以覆盖其中的消息。
+
+这样既能让产品文案始终由应用控制,也不会重复维护共享包的翻译。
+
+## 工作流 {#workflow}
+
+1. 配置语言和词典来源。
+2. 安装 Vite 插件。
+3. 将一个语言词典加载到 `I18nProvider`。
+4. 提取并翻译应用消息。
+5. 开发时使用 Devtool 或 Message Studio。
+6. 编译紧凑且可缓存的生产资源。
+
+## 包入口 {#package-entries}
+
+- `@workspace/i18n` 提供 React 运行时。
+- `@workspace/i18n/catalogs` 加载生成的词典。
+- `@workspace/i18n/devtool` 提供开发界面。
+- `@workspace/i18n/vite` 配置 Vite。
+- `workspace-i18n` 提供命令行工具。
diff --git a/packages/lexical/docs/en-US/advanced/custom-actions.mdx b/packages/lexical/docs/en-US/advanced/custom-actions.mdx
new file mode 100644
index 0000000..4b6657c
--- /dev/null
+++ b/packages/lexical/docs/en-US/advanced/custom-actions.mdx
@@ -0,0 +1,44 @@
+---
+title: Define an action
+description: Add product-specific behavior while preserving automatic dependency collection.
+order: 20
+toc:
+ - id: declare-the-action
+ title: Declare the action
+ - id: custom-control
+ title: Custom control
+ - id: typed-values
+ title: Typed values
+---
+
+## Declare the action {#declare-the-action}
+
+Keep the command and its editor dependencies in one definition:
+
+```tsx
+const Mention = defineLexicalAction({
+ name: "mention",
+ label: "Insert mention",
+ nodes: [MentionNode],
+ plugins: [MentionPopoverPlugin],
+ execute: ({ editor }) => openMentionPicker(editor),
+})
+```
+
+The node and plugin are enabled whenever `` appears inside `LexicalActions`.
+
+## Custom control {#custom-control}
+
+```tsx
+
+ {({ disabled, execute }) => (
+
+ )}
+
+```
+
+Replacing the visible control does not change dependency collection.
+
+## Typed values {#typed-values}
+
+The render context exposes `execute(value?)` with the action's value type. `onClick` is the no-argument shortcut for ordinary buttons.
diff --git a/packages/lexical/docs/en-US/advanced/localization.mdx b/packages/lexical/docs/en-US/advanced/localization.mdx
new file mode 100644
index 0000000..099b376
--- /dev/null
+++ b/packages/lexical/docs/en-US/advanced/localization.mdx
@@ -0,0 +1,30 @@
+---
+title: Localize the editor
+description: Connect built-in labels and custom actions to the workspace internationalization runtime.
+order: 21
+toc:
+ - id: add-the-catalog
+ title: Add the catalog
+ - id: fallback
+ title: Fallback
+ - id: custom-labels
+ title: Custom labels
+---
+
+## Add the catalog {#add-the-catalog}
+
+```json
+{
+ "catalogSources": ["@workspace/lexical/locales/{locale}"]
+}
+```
+
+The package ships American English (`en-US`) and Simplified Chinese (`zh-Hans`) editor catalogs.
+
+## Fallback {#fallback}
+
+Built-in controls follow the active `@workspace/i18n` provider. Outside a provider, they fall back to English so the editor remains usable in isolated previews and tests.
+
+## Custom labels {#custom-labels}
+
+An external action label can be a plain string or a message descriptor containing `id` and `message`. Descriptors participate in the same extraction workflow as application copy.
diff --git a/packages/lexical/docs/en-US/examples/media.mdx b/packages/lexical/docs/en-US/examples/media.mdx
new file mode 100644
index 0000000..aee1bfd
--- /dev/null
+++ b/packages/lexical/docs/en-US/examples/media.mdx
@@ -0,0 +1,46 @@
+---
+title: Add media
+description: Insert uploaded media and resolve images pasted or dragged into the editor.
+order: 30
+toc:
+ - id: image-and-video
+ title: Image and video
+ - id: clipboard-images
+ title: Clipboard images
+ - id: editing-media
+ title: Editing media
+---
+
+## Image and video {#image-and-video}
+
+The default `Image` and `Video` actions open built-in input dialogs. A custom uploader can hand the completed payload directly to the action:
+
+```tsx
+
+ {({ execute }) => (
+ execute({ src, alt, caption })}
+ />
+ )}
+
+```
+
+## Clipboard images {#clipboard-images}
+
+The full preset includes `ClipboardImages`. For production, resolve pasted and dropped files through object storage:
+
+```tsx
+ {
+ const uploaded = await uploadImage(file, {
+ signal,
+ onProgress: reportProgress,
+ })
+ return { alt: file.name, src: uploaded.url }
+ }}
+/>
+```
+
+## Editing media {#editing-media}
+
+Selected images support resizing, captions, alignment, and visible or keyboard deletion. Non-image clipboard files remain under normal browser handling.
diff --git a/packages/lexical/docs/en-US/guide/actions.mdx b/packages/lexical/docs/en-US/guide/actions.mdx
new file mode 100644
index 0000000..0d3c38d
--- /dev/null
+++ b/packages/lexical/docs/en-US/guide/actions.mdx
@@ -0,0 +1,42 @@
+---
+title: Arrange actions
+description: Replace the preset with explicit actions and place controls in the correct editing surfaces.
+order: 11
+toc:
+ - id: explicit-actions
+ title: Explicit actions
+ - id: placement
+ title: Placement
+ - id: action-groups
+ title: Action groups
+---
+
+## Explicit actions {#explicit-actions}
+
+Switch from a preset to children when the product needs exact feature and ordering control:
+
+```tsx
+
+
+
+
+
+
+
+```
+
+## Placement {#placement}
+
+Actions default to the fixed toolbar. The `in` prop accepts one area or several:
+
+```tsx
+
+
+
+```
+
+Hidden actions provide editor behavior without rendering a control. `DraggableBlocks` uses this pattern.
+
+## Action groups {#action-groups}
+
+`ActionGroup` can be a logical group or a visible menu. Use a menu for mutually related choices such as normal text, headings, quotes, and list styles.
diff --git a/packages/lexical/docs/en-US/guide/setup.mdx b/packages/lexical/docs/en-US/guide/setup.mdx
new file mode 100644
index 0000000..de58e3a
--- /dev/null
+++ b/packages/lexical/docs/en-US/guide/setup.mdx
@@ -0,0 +1,47 @@
+---
+title: Create the editor
+description: Render the root, content surface, toolbars, and a complete default action set.
+order: 10
+toc:
+ - id: styles
+ title: Styles
+ - id: editor-structure
+ title: Editor structure
+ - id: presets
+ title: Presets
+---
+
+## Styles {#styles}
+
+Import the editor stylesheet once in the application:
+
+```ts
+import "@workspace/lexical/globals.css"
+```
+
+## Editor structure {#editor-structure}
+
+```tsx
+import {
+ LexicalActions,
+ LexicalBubbleToolbar,
+ LexicalContent,
+ LexicalFixedToolbar,
+ LexicalFooter,
+ LexicalRoot,
+} from "@workspace/lexical"
+
+;
+
+
+
+
+
+
+```
+
+Toolbars and the footer render no DOM when their region has no actions and no custom children.
+
+## Presets {#presets}
+
+Choose `minimal` for basic text editing or `full` for the complete built-in feature set. Preset mode intentionally does not accept manual action children.
diff --git a/packages/lexical/docs/en-US/overview.mdx b/packages/lexical/docs/en-US/overview.mdx
new file mode 100644
index 0000000..42a15b3
--- /dev/null
+++ b/packages/lexical/docs/en-US/overview.mdx
@@ -0,0 +1,26 @@
+---
+title: Overview
+description: Learn the editor's declarative action model before assembling a complete editing surface.
+order: 1
+toc:
+ - id: mental-model
+ title: Mental model
+ - id: tutorial-map
+ title: Tutorial map
+ - id: stable-capabilities
+ title: Stable capabilities
+---
+
+## Mental model {#mental-model}
+
+`@workspace/lexical` treats editor actions as declarations. Each action describes its behavior and the nodes, plugins, or embeds it requires. `LexicalRoot` collects those requirements before creating the editor.
+
+Toolbars decide where an action appears; they do not separately register editor capabilities.
+
+## Tutorial map {#tutorial-map}
+
+You will create the root and content surface, choose a preset, customize action placement, define an application action, add media uploads, and finish with localized labels.
+
+## Stable capabilities {#stable-capabilities}
+
+Actions, nodes, and plugins are fixed when an editor instance is created. Give `LexicalRoot` a new React `key` when the application needs to replace the entire capability set.
diff --git a/packages/lexical/docs/zh-Hans/advanced/custom-actions.mdx b/packages/lexical/docs/zh-Hans/advanced/custom-actions.mdx
new file mode 100644
index 0000000..e361ca6
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/advanced/custom-actions.mdx
@@ -0,0 +1,44 @@
+---
+title: 定义 Action
+description: 在保留自动依赖收集能力的同时添加产品专属行为。
+order: 20
+toc:
+ - id: declare-the-action
+ title: 声明 Action
+ - id: custom-control
+ title: 自定义控件
+ - id: typed-values
+ title: 类型化参数
+---
+
+## 声明 Action {#declare-the-action}
+
+将命令与其编辑器依赖放在同一个定义中:
+
+```tsx
+const Mention = defineLexicalAction({
+ name: "mention",
+ label: "插入提及",
+ nodes: [MentionNode],
+ plugins: [MentionPopoverPlugin],
+ execute: ({ editor }) => openMentionPicker(editor),
+})
+```
+
+只要 `` 出现在 `LexicalActions` 内,它需要的节点和插件就会自动启用。
+
+## 自定义控件 {#custom-control}
+
+```tsx
+
+ {({ disabled, execute }) => (
+
+ )}
+
+```
+
+替换可见控件不会改变依赖收集结果。
+
+## 类型化参数 {#typed-values}
+
+渲染上下文会公开带有 Action 参数类型的 `execute(value?)`。普通按钮可以使用无参数快捷方式 `onClick`。
diff --git a/packages/lexical/docs/zh-Hans/advanced/localization.mdx b/packages/lexical/docs/zh-Hans/advanced/localization.mdx
new file mode 100644
index 0000000..41d8d2b
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/advanced/localization.mdx
@@ -0,0 +1,30 @@
+---
+title: 编辑器本地化
+description: 将内置标签和自定义 Actions 接入工作区国际化运行时。
+order: 21
+toc:
+ - id: add-the-catalog
+ title: 添加词典
+ - id: fallback
+ title: 回退行为
+ - id: custom-labels
+ title: 自定义标签
+---
+
+## 添加词典 {#add-the-catalog}
+
+```json
+{
+ "catalogSources": ["@workspace/lexical/locales/{locale}"]
+}
+```
+
+该包提供简体中文(`zh-Hans`)和美式英语(`en-US`)编辑器词典。
+
+## 回退行为 {#fallback}
+
+内置控件跟随当前 `@workspace/i18n` Provider。在 Provider 外部会回退为英文,使编辑器在独立预览和测试中仍可使用。
+
+## 自定义标签 {#custom-labels}
+
+外部 Action 标签可以是普通字符串,也可以是包含 `id` 和 `message` 的消息描述符。描述符会参与和应用文案相同的提取流程。
diff --git a/packages/lexical/docs/zh-Hans/examples/media.mdx b/packages/lexical/docs/zh-Hans/examples/media.mdx
new file mode 100644
index 0000000..57fd0b2
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/examples/media.mdx
@@ -0,0 +1,46 @@
+---
+title: 添加媒体
+description: 插入已上传媒体,并处理粘贴或拖入编辑器的图片。
+order: 30
+toc:
+ - id: image-and-video
+ title: 图片和视频
+ - id: clipboard-images
+ title: 剪贴板图片
+ - id: editing-media
+ title: 编辑媒体
+---
+
+## 图片和视频 {#image-and-video}
+
+默认的 `Image` 和 `Video` Actions 会打开内置输入对话框。自定义上传器可以把完成后的数据直接交给 Action:
+
+```tsx
+
+ {({ execute }) => (
+ execute({ src, alt, caption })}
+ />
+ )}
+
+```
+
+## 剪贴板图片 {#clipboard-images}
+
+完整预设包含 `ClipboardImages`。生产环境应通过对象存储解析粘贴和拖放的文件:
+
+```tsx
+ {
+ const uploaded = await uploadImage(file, {
+ signal,
+ onProgress: reportProgress,
+ })
+ return { alt: file.name, src: uploaded.url }
+ }}
+/>
+```
+
+## 编辑媒体 {#editing-media}
+
+选中的图片支持调整尺寸、编辑说明、对齐,以及可见按钮或键盘删除。非图片剪贴板文件继续交由浏览器的默认行为处理。
diff --git a/packages/lexical/docs/zh-Hans/guide/actions.mdx b/packages/lexical/docs/zh-Hans/guide/actions.mdx
new file mode 100644
index 0000000..ce3fbd4
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/guide/actions.mdx
@@ -0,0 +1,42 @@
+---
+title: 编排操作
+description: 用显式 Actions 替换预设,并将控件放入正确的编辑区域。
+order: 11
+toc:
+ - id: explicit-actions
+ title: 显式 Actions
+ - id: placement
+ title: 放置位置
+ - id: action-groups
+ title: Action 分组
+---
+
+## 显式 Actions {#explicit-actions}
+
+当产品需要精确控制功能和顺序时,将预设切换为子元素:
+
+```tsx
+
+
+
+
+
+
+
+```
+
+## 放置位置 {#placement}
+
+Actions 默认显示在固定工具栏中。`in` 属性接受一个或多个区域:
+
+```tsx
+
+
+
+```
+
+隐藏 Action 可以提供编辑器行为而不渲染控件,`DraggableBlocks` 就使用了这种模式。
+
+## Action 分组 {#action-groups}
+
+`ActionGroup` 可以是逻辑分组,也可以是可见菜单。普通文本、标题、引用和列表样式等相互关联的选择适合放入菜单。
diff --git a/packages/lexical/docs/zh-Hans/guide/setup.mdx b/packages/lexical/docs/zh-Hans/guide/setup.mdx
new file mode 100644
index 0000000..59edcfe
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/guide/setup.mdx
@@ -0,0 +1,47 @@
+---
+title: 创建编辑器
+description: 渲染根节点、内容区域、工具栏和完整的默认 Action 集合。
+order: 10
+toc:
+ - id: styles
+ title: 样式
+ - id: editor-structure
+ title: 编辑器结构
+ - id: presets
+ title: 预设
+---
+
+## 样式 {#styles}
+
+在应用中导入一次编辑器样式:
+
+```ts
+import "@workspace/lexical/globals.css"
+```
+
+## 编辑器结构 {#editor-structure}
+
+```tsx
+import {
+ LexicalActions,
+ LexicalBubbleToolbar,
+ LexicalContent,
+ LexicalFixedToolbar,
+ LexicalFooter,
+ LexicalRoot,
+} from "@workspace/lexical"
+
+;
+
+
+
+
+
+
+```
+
+当某个区域没有 Actions 或自定义子元素时,对应工具栏和 Footer 不会渲染 DOM。
+
+## 预设 {#presets}
+
+基础文本编辑选择 `minimal`,完整内置功能选择 `full`。预设模式有意不接受手动 Action 子元素。
diff --git a/packages/lexical/docs/zh-Hans/overview.mdx b/packages/lexical/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..8e5285a
--- /dev/null
+++ b/packages/lexical/docs/zh-Hans/overview.mdx
@@ -0,0 +1,26 @@
+---
+title: 概览
+description: 在组装完整编辑界面之前,先理解编辑器的声明式 Action 模型。
+order: 1
+toc:
+ - id: mental-model
+ title: 心智模型
+ - id: tutorial-map
+ title: 教程路线
+ - id: stable-capabilities
+ title: 稳定的能力集合
+---
+
+## 心智模型 {#mental-model}
+
+`@workspace/lexical` 将编辑器操作视为声明。每个 Action 描述自身行为及其需要的节点、插件或嵌入内容。`LexicalRoot` 会在创建编辑器前收集这些依赖。
+
+工具栏只决定 Action 出现的位置,不负责单独注册编辑器能力。
+
+## 教程路线 {#tutorial-map}
+
+你将创建根节点和内容区域、选择预设、自定义 Action 位置、定义应用专属 Action、添加媒体上传,最后接入本地化标签。
+
+## 稳定的能力集合 {#stable-capabilities}
+
+编辑器实例创建后,其 Actions、节点和插件保持固定。当应用需要替换整套能力时,请为 `LexicalRoot` 提供新的 React `key`。
diff --git a/packages/preferences/docs/en-US/advanced/lifecycle.mdx b/packages/preferences/docs/en-US/advanced/lifecycle.mdx
new file mode 100644
index 0000000..95ddbb6
--- /dev/null
+++ b/packages/preferences/docs/en-US/advanced/lifecycle.mdx
@@ -0,0 +1,57 @@
+---
+title: Lifecycle and persistence
+description: Coordinate SSR, asynchronous persistence, browser synchronization, and external input validation.
+order: 20
+toc:
+ - id: server-snapshots
+ title: Server snapshots
+ - id: persist-updates
+ title: Persist updates
+ - id: preference-effects
+ title: Preference effects
+ - id: validate-updates
+ title: Validate updates
+---
+
+## Server snapshots {#server-snapshots}
+
+`initialPreferences` is captured when the provider creates its store. It remains the server and hydration snapshot; changing that prop later does not reset local preferences. Remount the provider when the application intentionally switches to a different preference identity.
+
+## Persist updates {#persist-updates}
+
+`onPreferenceChange` runs only when a value actually changes. It may return a promise, but local updates are not blocked while persistence completes. Handle retries, errors, and rollback behavior in the application boundary.
+
+## Preference effects {#preference-effects}
+
+Effects connect preferences to APIs outside React:
+
+```tsx
+const documentEffects = [
+ {
+ layoutEffect: ({ preferences }) => {
+ document.documentElement.dataset.theme = preferences["theme-mode"]
+ },
+ effect: ({ store }) => {
+ const media = window.matchMedia("(prefers-color-scheme: dark)")
+ const listener = () => synchronizeTheme(store.getSnapshot(), media)
+
+ media.addEventListener("change", listener)
+ return () => media.removeEventListener("change", listener)
+ },
+ },
+] satisfies readonly PreferenceEffect[]
+```
+
+Use `layoutEffect` for DOM changes that must happen before paint. Use `effect` for subscriptions. Keep the effects array reference stable because callbacks and cleanup functions run again whenever the snapshot or array changes.
+
+## Validate updates {#validate-updates}
+
+Create a type guard from the same definitions before accepting data from an untyped boundary:
+
+```ts
+const isPreferenceUpdate = createPreferenceUpdateGuard(definitions)
+
+if (isPreferenceUpdate(payload)) {
+ await savePreference(payload)
+}
+```
diff --git a/packages/preferences/docs/en-US/examples/theme-example.mdx b/packages/preferences/docs/en-US/examples/theme-example.mdx
new file mode 100644
index 0000000..b514a34
--- /dev/null
+++ b/packages/preferences/docs/en-US/examples/theme-example.mdx
@@ -0,0 +1,67 @@
+---
+title: Theme preference
+description: Build a complete light, dark, and system theme preference flow.
+order: 30
+toc:
+ - id: define-theme-mode
+ title: Define theme mode
+ - id: apply-the-theme
+ title: Apply the theme
+ - id: render-a-control
+ title: Render a control
+---
+
+## Define theme mode {#define-theme-mode}
+
+```ts
+import "@workspace/preferences"
+
+declare module "@workspace/preferences" {
+ interface PreferencesCustom {
+ "theme-mode": "light" | "dark" | "system"
+ }
+}
+```
+
+Load the cookie on the server and pass the parsed value through `initialPreferences` so server markup and hydration share the same snapshot.
+
+## Apply the theme {#apply-the-theme}
+
+```tsx
+const themeEffects = [
+ {
+ layoutEffect: ({ preferences }) => {
+ const systemDark = window.matchMedia(
+ "(prefers-color-scheme: dark)"
+ ).matches
+ const dark =
+ preferences["theme-mode"] === "dark" ||
+ (preferences["theme-mode"] === "system" && systemDark)
+
+ document.documentElement.classList.toggle("dark", dark)
+ document.documentElement.style.colorScheme = dark ? "dark" : "light"
+ },
+ },
+] satisfies readonly PreferenceEffect[]
+```
+
+## Render a control {#render-a-control}
+
+```tsx
+function ThemeModeSelect() {
+ const [themeMode, setThemeMode] = usePreference("theme-mode")
+
+ return (
+
+ )
+}
+```
diff --git a/packages/preferences/docs/en-US/guide/setup.mdx b/packages/preferences/docs/en-US/guide/setup.mdx
new file mode 100644
index 0000000..8648f79
--- /dev/null
+++ b/packages/preferences/docs/en-US/guide/setup.mdx
@@ -0,0 +1,74 @@
+---
+title: Define and provide preferences
+description: Declare the application preference shape and mount the provider with an SSR-safe snapshot.
+order: 10
+toc:
+ - id: declare-preferences
+ title: Declare preferences
+ - id: define-defaults
+ title: Define defaults
+ - id: mount-the-provider
+ title: Mount the provider
+---
+
+## Declare preferences {#declare-preferences}
+
+Use module augmentation in an application-owned TypeScript module:
+
+```ts
+import "@workspace/preferences"
+
+declare module "@workspace/preferences" {
+ interface PreferencesCustom {
+ "navigation-density": "comfortable" | "compact"
+ "theme-mode": "light" | "dark" | "system"
+ }
+}
+```
+
+The declaration updates `Preferences`, `PreferenceKey`, `PreferenceValue`, `PreferenceUpdate`, and both preference hooks.
+
+## Define defaults {#define-defaults}
+
+Definitions keep parsing, serialization, defaults, and runtime validation together:
+
+```ts
+const definitions = {
+ "navigation-density": {
+ cookie: "navigation-density",
+ defaultValue: "comfortable",
+ is: (value) => value === "comfortable" || value === "compact",
+ parse: (value) => (value === "compact" ? "compact" : "comfortable"),
+ serialize: (value) => value,
+ },
+ "theme-mode": {
+ cookie: "theme-mode",
+ defaultValue: "system",
+ is: (value) => value === "light" || value === "dark" || value === "system",
+ parse: (value) =>
+ value === "light" || value === "dark" ? value : "system",
+ serialize: (value) => value,
+ },
+} satisfies PreferenceDefinitions
+
+const defaultPreferences = createDefaultPreferences(definitions)
+```
+
+## Mount the provider {#mount-the-provider}
+
+Pass the complete server-derived snapshot to the provider:
+
+```tsx
+ savePreference(update)}
+>
+
+
+```
+
+Read one preference with an API matching `useState`:
+
+```tsx
+const [themeMode, setThemeMode] = usePreference("theme-mode")
+```
diff --git a/packages/preferences/docs/en-US/overview.mdx b/packages/preferences/docs/en-US/overview.mdx
new file mode 100644
index 0000000..afeb945
--- /dev/null
+++ b/packages/preferences/docs/en-US/overview.mdx
@@ -0,0 +1,28 @@
+---
+title: Overview
+description: Understand the role and boundaries of the Preferences package.
+order: 1
+toc:
+ - id: package-role
+ title: Package role
+ - id: capabilities
+ title: Capabilities
+ - id: design-boundary
+ title: Design boundary
+---
+
+## Package role {#package-role}
+
+`@workspace/preferences` is a small React store for application preferences such as theme mode, navigation density, and compact layouts. It keeps the preference vocabulary owned by the consuming application.
+
+## Capabilities {#capabilities}
+
+- Extend the preference shape through TypeScript module augmentation.
+- Hydrate a deterministic server snapshot with `useSyncExternalStore`.
+- Persist typed `{ key, value }` updates through an application callback.
+- Synchronize preferences with browser APIs through composable effects.
+- Validate external updates from cookies, requests, forms, or message channels.
+
+## Design boundary {#design-boundary}
+
+The package does not define product-specific keys and does not select a persistence service. The application supplies its complete initial snapshot and decides how accepted changes are stored.
diff --git a/packages/preferences/docs/zh-Hans/advanced/lifecycle.mdx b/packages/preferences/docs/zh-Hans/advanced/lifecycle.mdx
new file mode 100644
index 0000000..81c5dcb
--- /dev/null
+++ b/packages/preferences/docs/zh-Hans/advanced/lifecycle.mdx
@@ -0,0 +1,57 @@
+---
+title: 生命周期与持久化
+description: 协调 SSR、异步持久化、浏览器同步和外部输入校验。
+order: 20
+toc:
+ - id: server-snapshots
+ title: 服务端快照
+ - id: persist-updates
+ title: 持久化更新
+ - id: preference-effects
+ title: 偏好副作用
+ - id: validate-updates
+ title: 校验更新
+---
+
+## 服务端快照 {#server-snapshots}
+
+Provider 创建 Store 时会捕获 `initialPreferences`。它会一直作为服务端和水合快照;之后改变这个属性不会重置本地偏好。当应用有意切换到另一套偏好身份时,应重新挂载 Provider。
+
+## 持久化更新 {#persist-updates}
+
+`onPreferenceChange` 只在值确实发生变化时运行。它可以返回 Promise,但持久化过程不会阻塞本地更新。重试、错误和回滚行为应在应用边界处理。
+
+## 偏好副作用 {#preference-effects}
+
+副作用用于连接偏好与 React 之外的 API:
+
+```tsx
+const documentEffects = [
+ {
+ layoutEffect: ({ preferences }) => {
+ document.documentElement.dataset.theme = preferences["theme-mode"]
+ },
+ effect: ({ store }) => {
+ const media = window.matchMedia("(prefers-color-scheme: dark)")
+ const listener = () => synchronizeTheme(store.getSnapshot(), media)
+
+ media.addEventListener("change", listener)
+ return () => media.removeEventListener("change", listener)
+ },
+ },
+] satisfies readonly PreferenceEffect[]
+```
+
+必须在绘制前完成的 DOM 变更使用 `layoutEffect`,订阅使用 `effect`。副作用数组的引用应保持稳定,因为快照或数组变化时,回调和清理函数都会重新运行。
+
+## 校验更新 {#validate-updates}
+
+在接受无类型边界的数据前,使用相同的 Definitions 创建类型守卫:
+
+```ts
+const isPreferenceUpdate = createPreferenceUpdateGuard(definitions)
+
+if (isPreferenceUpdate(payload)) {
+ await savePreference(payload)
+}
+```
diff --git a/packages/preferences/docs/zh-Hans/examples/theme-example.mdx b/packages/preferences/docs/zh-Hans/examples/theme-example.mdx
new file mode 100644
index 0000000..cd1c8cf
--- /dev/null
+++ b/packages/preferences/docs/zh-Hans/examples/theme-example.mdx
@@ -0,0 +1,67 @@
+---
+title: 主题偏好示例
+description: 构建完整的亮色、暗色和跟随系统主题偏好流程。
+order: 30
+toc:
+ - id: define-theme-mode
+ title: 定义主题模式
+ - id: apply-the-theme
+ title: 应用主题
+ - id: render-a-control
+ title: 渲染控件
+---
+
+## 定义主题模式 {#define-theme-mode}
+
+```ts
+import "@workspace/preferences"
+
+declare module "@workspace/preferences" {
+ interface PreferencesCustom {
+ "theme-mode": "light" | "dark" | "system"
+ }
+}
+```
+
+在服务端读取 Cookie,并通过 `initialPreferences` 传入解析后的值,使服务端标记和水合过程使用同一个快照。
+
+## 应用主题 {#apply-the-theme}
+
+```tsx
+const themeEffects = [
+ {
+ layoutEffect: ({ preferences }) => {
+ const systemDark = window.matchMedia(
+ "(prefers-color-scheme: dark)"
+ ).matches
+ const dark =
+ preferences["theme-mode"] === "dark" ||
+ (preferences["theme-mode"] === "system" && systemDark)
+
+ document.documentElement.classList.toggle("dark", dark)
+ document.documentElement.style.colorScheme = dark ? "dark" : "light"
+ },
+ },
+] satisfies readonly PreferenceEffect[]
+```
+
+## 渲染控件 {#render-a-control}
+
+```tsx
+function ThemeModeSelect() {
+ const [themeMode, setThemeMode] = usePreference("theme-mode")
+
+ return (
+
+ )
+}
+```
diff --git a/packages/preferences/docs/zh-Hans/guide/setup.mdx b/packages/preferences/docs/zh-Hans/guide/setup.mdx
new file mode 100644
index 0000000..933057e
--- /dev/null
+++ b/packages/preferences/docs/zh-Hans/guide/setup.mdx
@@ -0,0 +1,74 @@
+---
+title: 定义并提供偏好设置
+description: 声明应用偏好类型,并使用 SSR 安全快照挂载 Provider。
+order: 10
+toc:
+ - id: declare-preferences
+ title: 声明偏好设置
+ - id: define-defaults
+ title: 定义默认值
+ - id: mount-the-provider
+ title: 挂载 Provider
+---
+
+## 声明偏好设置 {#declare-preferences}
+
+在应用拥有的 TypeScript 模块中使用模块扩充:
+
+```ts
+import "@workspace/preferences"
+
+declare module "@workspace/preferences" {
+ interface PreferencesCustom {
+ "navigation-density": "comfortable" | "compact"
+ "theme-mode": "light" | "dark" | "system"
+ }
+}
+```
+
+该声明会同时更新 `Preferences`、`PreferenceKey`、`PreferenceValue`、`PreferenceUpdate` 和两个偏好 Hooks 的类型。
+
+## 定义默认值 {#define-defaults}
+
+Definition 将解析、序列化、默认值和运行时校验放在一起:
+
+```ts
+const definitions = {
+ "navigation-density": {
+ cookie: "navigation-density",
+ defaultValue: "comfortable",
+ is: (value) => value === "comfortable" || value === "compact",
+ parse: (value) => (value === "compact" ? "compact" : "comfortable"),
+ serialize: (value) => value,
+ },
+ "theme-mode": {
+ cookie: "theme-mode",
+ defaultValue: "system",
+ is: (value) => value === "light" || value === "dark" || value === "system",
+ parse: (value) =>
+ value === "light" || value === "dark" ? value : "system",
+ serialize: (value) => value,
+ },
+} satisfies PreferenceDefinitions
+
+const defaultPreferences = createDefaultPreferences(definitions)
+```
+
+## 挂载 Provider {#mount-the-provider}
+
+将服务端得到的完整快照传给 Provider:
+
+```tsx
+ savePreference(update)}
+>
+
+
+```
+
+通过类似 `useState` 的 API 读取单个偏好:
+
+```tsx
+const [themeMode, setThemeMode] = usePreference("theme-mode")
+```
diff --git a/packages/preferences/docs/zh-Hans/overview.mdx b/packages/preferences/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..1d0f637
--- /dev/null
+++ b/packages/preferences/docs/zh-Hans/overview.mdx
@@ -0,0 +1,28 @@
+---
+title: 概览
+description: 了解 Preferences 包的职责和边界。
+order: 1
+toc:
+ - id: package-role
+ title: 包的职责
+ - id: capabilities
+ title: 主要能力
+ - id: design-boundary
+ title: 设计边界
+---
+
+## 包的职责 {#package-role}
+
+`@workspace/preferences` 是一个用于管理应用偏好的轻量 React Store,例如主题模式、导航密度和紧凑布局。偏好字段仍由使用它的应用定义。
+
+## 主要能力 {#capabilities}
+
+- 通过 TypeScript 模块扩充扩展偏好类型。
+- 使用 `useSyncExternalStore` 水合确定的服务端快照。
+- 通过应用回调持久化类型安全的 `{ key, value }` 更新。
+- 通过可组合副作用与浏览器 API 同步偏好。
+- 校验来自 Cookie、请求、表单或消息通道的外部更新。
+
+## 设计边界 {#design-boundary}
+
+该包不定义产品特有字段,也不指定持久化服务。应用负责提供完整的初始快照,并决定如何保存已接受的变更。
diff --git a/packages/search/docs/en-US/advanced/adapter-and-pagination.mdx b/packages/search/docs/en-US/advanced/adapter-and-pagination.mdx
new file mode 100644
index 0000000..98e21cb
--- /dev/null
+++ b/packages/search/docs/en-US/advanced/adapter-and-pagination.mdx
@@ -0,0 +1,42 @@
+---
+title: Adapter and pagination
+description: Implement cancellable search requests and return mergeable grouped pages.
+order: 20
+toc:
+ - id: implement-adapter
+ title: Implement the adapter
+ - id: group-results
+ title: Group results
+ - id: paginate
+ title: Paginate
+---
+
+## Implement the adapter {#implement-adapter}
+
+`SearchAdapter` is the package's only data dependency:
+
+```ts
+const searchAdapter: SearchAdapter = {
+ async search({ cursor, query, signal }) {
+ const response = await fetch(
+ `/api/search?q=${encodeURIComponent(query)}&cursor=${cursor ?? ""}`,
+ { signal }
+ )
+
+ if (!response.ok) throw new Error("Search request failed")
+ return response.json()
+ },
+}
+```
+
+Pass `signal` to the underlying request so an older response cannot replace a newer query.
+
+## Group results {#group-results}
+
+Each `SearchPage` contains `SearchResultGroup` values. Keep a group's `id` stable between pages and each item's `id` stable inside its group. Search uses both identifiers to merge results and remove duplicates.
+
+Use `payload` for application data and `icon` or `image` to customize result presentation.
+
+## Paginate {#paginate}
+
+Return `nextCursor` when another page is available; return `null` or omit it at the end. The surface only displays “Load more” while a cursor exists. A cursor can be a string or number and is passed back to the adapter unchanged.
diff --git a/packages/search/docs/en-US/examples/global-search.mdx b/packages/search/docs/en-US/examples/global-search.mdx
new file mode 100644
index 0000000..30a88b3
--- /dev/null
+++ b/packages/search/docs/en-US/examples/global-search.mdx
@@ -0,0 +1,58 @@
+---
+title: Global search example
+description: Compose a trigger, dialog, recent searches, and result navigation.
+order: 30
+toc:
+ - id: define-results
+ title: Define results
+ - id: handle-selection
+ title: Handle selection
+ - id: control-history
+ title: Control history
+---
+
+## Define results {#define-results}
+
+```ts
+const searchAdapter: SearchAdapter = {
+ async search({ query }) {
+ return {
+ groups: [
+ {
+ id: "docs",
+ label: "Documentation",
+ items: documents
+ .filter((document) => document.title.includes(query))
+ .map((document) => ({
+ id: document.slug,
+ title: document.title,
+ description: document.description,
+ payload: { href: `/docs/${document.slug}` },
+ })),
+ },
+ ],
+ }
+ },
+}
+```
+
+## Handle selection {#handle-selection}
+
+```tsx
+ {
+ const payload = item.payload as { href: string }
+ router.navigate({ to: payload.href })
+ }}
+>
+
+
+
+```
+
+Code that cannot render `SearchTrigger` can call `searchDialogHandle.open(null)`.
+
+## Control history {#control-history}
+
+Recent searches use `workspace-search-history` by default. Supply a product-specific `historyStorageKey`, or set it to `false` to keep history in memory for the current session only.
diff --git a/packages/search/docs/en-US/guide/setup.mdx b/packages/search/docs/en-US/guide/setup.mdx
new file mode 100644
index 0000000..fd99c47
--- /dev/null
+++ b/packages/search/docs/en-US/guide/setup.mdx
@@ -0,0 +1,47 @@
+---
+title: Installation and setup
+description: Add Search, register its catalog, and mount the search surface.
+order: 10
+toc:
+ - id: add-the-package
+ title: Add the package
+ - id: import-styles
+ title: Import styles
+ - id: mount-search
+ title: Mount search
+---
+
+## Add the package {#add-the-package}
+
+Add Search to the consuming application:
+
+```json
+{
+ "dependencies": {
+ "@workspace/search": "workspace:*"
+ }
+}
+```
+
+## Import styles {#import-styles}
+
+Import the Search stylesheet once from the application's global stylesheet so Tailwind scans the utilities used by the package:
+
+```css
+@import "@workspace/search/globals.css";
+```
+
+Also add `@workspace/search/locales/{locale}` to the application's internationalization catalog sources.
+
+## Mount search {#mount-search}
+
+```tsx
+import { SearchDialog, SearchProvider, SearchTrigger } from "@workspace/search"
+
+;
+ Search
+
+
+```
+
+`SearchDialog` registers `Mod+K` by default. Pass `hotkey={false}` when the application manages shortcuts centrally.
diff --git a/packages/search/docs/en-US/overview.mdx b/packages/search/docs/en-US/overview.mdx
new file mode 100644
index 0000000..afabd21
--- /dev/null
+++ b/packages/search/docs/en-US/overview.mdx
@@ -0,0 +1,29 @@
+---
+title: Overview
+description: Understand the Search package data boundary, interactions, and use cases.
+order: 1
+toc:
+ - id: package-role
+ title: Package role
+ - id: capabilities
+ title: Capabilities
+ - id: design-boundary
+ title: Design boundary
+---
+
+## Package role {#package-role}
+
+`@workspace/search` provides an application-search surface and its state management. The host supplies data through `SearchAdapter`; Search owns the query lifecycle, pagination, result presentation, selection interactions, and recent searches.
+
+## Capabilities {#capabilities}
+
+- Connect any HTTP API, database, command registry, or local index through an adapter.
+- Present results in domain groups and merge paginated data.
+- Cancel stale requests with `AbortSignal`.
+- Keep recent searches with configurable local persistence.
+- Provide a `Mod+K` shortcut plus empty, loading, and error states.
+- Ship `en-US` and `zh-Hans` message catalogs.
+
+## Design boundary {#design-boundary}
+
+Search does not choose the data source or perform navigation. The application implements the adapter and interprets each result's `payload` in `onSelect`.
diff --git a/packages/search/docs/zh-Hans/advanced/adapter-and-pagination.mdx b/packages/search/docs/zh-Hans/advanced/adapter-and-pagination.mdx
new file mode 100644
index 0000000..17d69c9
--- /dev/null
+++ b/packages/search/docs/zh-Hans/advanced/adapter-and-pagination.mdx
@@ -0,0 +1,42 @@
+---
+title: Adapter 与分页
+description: 实现可取消的搜索请求,并返回可合并的分组分页结果。
+order: 20
+toc:
+ - id: implement-adapter
+ title: 实现 Adapter
+ - id: group-results
+ title: 组织结果
+ - id: paginate
+ title: 分页
+---
+
+## 实现 Adapter {#implement-adapter}
+
+`SearchAdapter` 是 Search 唯一的数据依赖:
+
+```ts
+const searchAdapter: SearchAdapter = {
+ async search({ cursor, query, signal }) {
+ const response = await fetch(
+ `/api/search?q=${encodeURIComponent(query)}&cursor=${cursor ?? ""}`,
+ { signal }
+ )
+
+ if (!response.ok) throw new Error("Search request failed")
+ return response.json()
+ },
+}
+```
+
+必须把 `signal` 传给底层请求,避免较早的响应覆盖较新的查询。
+
+## 组织结果 {#group-results}
+
+每个 `SearchPage` 包含若干 `SearchResultGroup`。Group 的 `id` 在分页之间应保持稳定;Item 的 `id` 在所属 Group 内应保持稳定。Search 会用这两个标识合并结果并消除重复项。
+
+可以通过 `payload` 携带应用数据,通过 `icon` 或 `image` 自定义结果外观。
+
+## 分页 {#paginate}
+
+还有后续数据时返回 `nextCursor`;没有更多结果时返回 `null` 或省略。界面只在存在游标时显示“加载更多”。游标可以是字符串或数字,并会原样传回 Adapter。
diff --git a/packages/search/docs/zh-Hans/examples/global-search.mdx b/packages/search/docs/zh-Hans/examples/global-search.mdx
new file mode 100644
index 0000000..56f7a73
--- /dev/null
+++ b/packages/search/docs/zh-Hans/examples/global-search.mdx
@@ -0,0 +1,58 @@
+---
+title: 全局搜索示例
+description: 组合触发器、对话框、历史记录和结果导航。
+order: 30
+toc:
+ - id: define-results
+ title: 定义结果
+ - id: handle-selection
+ title: 处理选择
+ - id: control-history
+ title: 控制历史记录
+---
+
+## 定义结果 {#define-results}
+
+```ts
+const searchAdapter: SearchAdapter = {
+ async search({ query }) {
+ return {
+ groups: [
+ {
+ id: "docs",
+ label: "文档",
+ items: documents
+ .filter((document) => document.title.includes(query))
+ .map((document) => ({
+ id: document.slug,
+ title: document.title,
+ description: document.description,
+ payload: { href: `/docs/${document.slug}` },
+ })),
+ },
+ ],
+ }
+ },
+}
+```
+
+## 处理选择 {#handle-selection}
+
+```tsx
+ {
+ const payload = item.payload as { href: string }
+ router.navigate({ to: payload.href })
+ }}
+>
+
+
+
+```
+
+不方便渲染 `SearchTrigger` 的位置可以调用 `searchDialogHandle.open(null)`。
+
+## 控制历史记录 {#control-history}
+
+历史记录默认保存在 `workspace-search-history`。为不同产品传入独立的 `historyStorageKey`,或者设置为 `false`,让历史只存在于当前内存会话中。
diff --git a/packages/search/docs/zh-Hans/guide/setup.mdx b/packages/search/docs/zh-Hans/guide/setup.mdx
new file mode 100644
index 0000000..47290ee
--- /dev/null
+++ b/packages/search/docs/zh-Hans/guide/setup.mdx
@@ -0,0 +1,47 @@
+---
+title: 安装与接入
+description: 添加 Search、注册翻译目录并挂载搜索界面。
+order: 10
+toc:
+ - id: add-the-package
+ title: 添加包
+ - id: import-styles
+ title: 导入样式
+ - id: mount-search
+ title: 挂载搜索
+---
+
+## 添加包 {#add-the-package}
+
+将 Search 加入应用依赖:
+
+```json
+{
+ "dependencies": {
+ "@workspace/search": "workspace:*"
+ }
+}
+```
+
+## 导入样式 {#import-styles}
+
+在应用的全局样式入口导入一次 Search 样式,使 Tailwind 扫描包内使用的 utilities:
+
+```css
+@import "@workspace/search/globals.css";
+```
+
+同时将 `@workspace/search/locales/{locale}` 加入应用的国际化目录来源。
+
+## 挂载搜索 {#mount-search}
+
+```tsx
+import { SearchDialog, SearchProvider, SearchTrigger } from "@workspace/search"
+
+;
+ 搜索
+
+
+```
+
+`SearchDialog` 默认注册 `Mod+K`。如果应用已经统一管理快捷键,可以传入 `hotkey={false}`。
diff --git a/packages/search/docs/zh-Hans/overview.mdx b/packages/search/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..3ca5629
--- /dev/null
+++ b/packages/search/docs/zh-Hans/overview.mdx
@@ -0,0 +1,29 @@
+---
+title: 概览
+description: 了解 Search 包的数据边界、交互能力和适用场景。
+order: 1
+toc:
+ - id: package-role
+ title: 包的职责
+ - id: capabilities
+ title: 主要能力
+ - id: design-boundary
+ title: 设计边界
+---
+
+## 包的职责 {#package-role}
+
+`@workspace/search` 提供应用级搜索界面与状态管理。宿主应用通过 `SearchAdapter` 提供数据,Search 负责查询生命周期、分页、结果展示、选择交互和搜索历史。
+
+## 主要能力 {#capabilities}
+
+- 使用 Adapter 连接任意 HTTP API、数据库、命令注册表或本地索引。
+- 按业务来源分组展示结果,并合并分页数据。
+- 使用 `AbortSignal` 取消已经过期的查询。
+- 提供最近搜索记录和可配置的本地持久化。
+- 内置 `Mod+K` 快捷键、无结果、加载与错误状态。
+- 提供 `en-US` 和 `zh-Hans` 消息目录。
+
+## 设计边界 {#design-boundary}
+
+Search 不决定数据从哪里获取,也不负责路由跳转。应用实现 Adapter,并在 `onSelect` 中解释结果的 `payload`。
diff --git a/packages/ui/docs/en-US/advanced/hooks.mdx b/packages/ui/docs/en-US/advanced/hooks.mdx
new file mode 100644
index 0000000..b6bd3f8
--- /dev/null
+++ b/packages/ui/docs/en-US/advanced/hooks.mdx
@@ -0,0 +1,31 @@
+---
+title: Add responsive behavior
+description: Use shared breakpoint and interaction hooks when CSS alone cannot express the behavior.
+order: 20
+toc:
+ - id: breakpoints
+ title: Breakpoints
+ - id: server-rendering
+ title: Server rendering
+ - id: ripple
+ title: Ripple interaction
+---
+
+## Breakpoints {#breakpoints}
+
+```tsx
+import { useBreakpoint, useIsMobile } from "@workspace/ui/hooks/use-breakpoint"
+
+const breakpoint = useBreakpoint()
+const isMobile = useIsMobile()
+```
+
+Prefer CSS responsive variants for visual changes. Use these hooks only when component behavior or mounted content must change.
+
+## Server rendering {#server-rendering}
+
+The breakpoint store uses a deterministic desktop server snapshot. `getBreakpointInitializationScript` can assign the matching root class before hydration when the application needs CSS and behavioral breakpoints to agree immediately.
+
+## Ripple interaction {#ripple}
+
+`useRipple` returns a ref callback that adds pointer-driven feedback to an interactive element. The element should establish a positioned containing block so the generated overlay uses the correct bounds.
diff --git a/packages/ui/docs/en-US/advanced/localization.mdx b/packages/ui/docs/en-US/advanced/localization.mdx
new file mode 100644
index 0000000..00ba31b
--- /dev/null
+++ b/packages/ui/docs/en-US/advanced/localization.mdx
@@ -0,0 +1,36 @@
+---
+title: Localize UI
+description: Load only the UI catalog and calendar locale required by the active language.
+order: 21
+toc:
+ - id: catalog-source
+ title: Catalog source
+ - id: direct-imports
+ title: Direct imports
+ - id: supported-locales
+ title: Supported locales
+---
+
+## Catalog source {#catalog-source}
+
+Add the UI locale source to the application's internationalization configuration:
+
+```json
+{
+ "catalogSources": ["@workspace/ui/locales/{locale}"]
+}
+```
+
+The locales root contains metadata and types. It does not import every language catalog.
+
+## Direct imports {#direct-imports}
+
+```ts
+import { calendarLocale, messages } from "@workspace/ui/locales/en-US"
+```
+
+Each language entry also exports its matching `react-day-picker` locale.
+
+## Supported locales {#supported-locales}
+
+The UI catalogs include American English (`en-US`) and Simplified Chinese (`zh-Hans`).
diff --git a/packages/ui/docs/en-US/examples/composition.mdx b/packages/ui/docs/en-US/examples/composition.mdx
new file mode 100644
index 0000000..7d4300e
--- /dev/null
+++ b/packages/ui/docs/en-US/examples/composition.mdx
@@ -0,0 +1,37 @@
+---
+title: Compose a form
+description: Combine field, input, and button primitives while keeping domain state in the application.
+order: 30
+toc:
+ - id: create-the-form
+ title: Create the form
+ - id: validation
+ title: Validation
+ - id: styling
+ title: Styling
+---
+
+## Create the form {#create-the-form}
+
+```tsx
+import { Button } from "@workspace/ui/components/button"
+import { Field, FieldError, FieldLabel } from "@workspace/ui/components/field"
+import { Input } from "@workspace/ui/components/input"
+
+;
+```
+
+## Validation {#validation}
+
+The UI package renders validation state but does not select a form library or schema. Connect native form data, React state, or a form framework at the application boundary.
+
+## Styling {#styling}
+
+Use component variants for supported semantic changes and `className` for local layout. Use `cn` from `@workspace/ui/lib/utils` when conditional classes or consumer overrides must be merged.
diff --git a/packages/ui/docs/en-US/guide/components.mdx b/packages/ui/docs/en-US/guide/components.mdx
new file mode 100644
index 0000000..ee0b297
--- /dev/null
+++ b/packages/ui/docs/en-US/guide/components.mdx
@@ -0,0 +1,30 @@
+---
+title: Choose components
+description: Select primitives by responsibility instead of importing a single monolithic component index.
+order: 11
+toc:
+ - id: forms
+ title: Forms and input
+ - id: overlays
+ title: Overlays and menus
+ - id: content
+ title: Content and feedback
+ - id: icons
+ title: Icons
+---
+
+## Forms and input {#forms}
+
+Use `Button`, `Input`, `Textarea`, `Field`, and `Label` for ordinary forms. Add `Checkbox`, `RadioGroup`, `Switch`, or `Slider` for choices, and `Select`, `Combobox`, `Command`, or `Calendar` for richer selection.
+
+## Overlays and menus {#overlays}
+
+`Dialog`, `AlertDialog`, `Sheet`, and `Drawer` cover modal surfaces. `Popover`, `HoverCard`, and `Tooltip` provide anchored information, while the menu modules cover context, dropdown, menubar, and navigation patterns.
+
+## Content and feedback {#content}
+
+Cards, items, tables, charts, progress, skeletons, spinners, empty states, messages, and toasts provide the common display vocabulary. Accordion, Collapsible, Tabs, Carousel, and Pagination organize larger content sets.
+
+## Icons {#icons}
+
+Import named icons from `@workspace/ui/components/icon`. The module centralizes the `@icones/react` vocabulary so applications and packages use the same icon source.
diff --git a/packages/ui/docs/en-US/guide/setup.mdx b/packages/ui/docs/en-US/guide/setup.mdx
new file mode 100644
index 0000000..0d28e83
--- /dev/null
+++ b/packages/ui/docs/en-US/guide/setup.mdx
@@ -0,0 +1,39 @@
+---
+title: Project setup
+description: Import the global stylesheet and begin with explicit component subpaths.
+order: 10
+toc:
+ - id: import-styles
+ title: Import styles
+ - id: import-a-component
+ title: Import a component
+ - id: entry-points
+ title: Entry points
+---
+
+## Import styles {#import-styles}
+
+Import the global stylesheet once at the application entry:
+
+```ts
+import "@workspace/ui/globals.css"
+```
+
+It provides the shared Tailwind layers, design variables, font setup, and utilities expected by the components.
+
+## Import a component {#import-a-component}
+
+```tsx
+import { Button } from "@workspace/ui/components/button"
+
+export function SaveButton() {
+ return
+}
+```
+
+## Entry points {#entry-points}
+
+- `@workspace/ui/components/*` exposes one component module.
+- `@workspace/ui/hooks/*` exposes reusable interactions and media-query state.
+- `@workspace/ui/lib/utils` exposes shared class-name composition.
+- `@workspace/ui/locales/*` exposes language-specific catalogs.
diff --git a/packages/ui/docs/en-US/overview.mdx b/packages/ui/docs/en-US/overview.mdx
new file mode 100644
index 0000000..83f874b
--- /dev/null
+++ b/packages/ui/docs/en-US/overview.mdx
@@ -0,0 +1,24 @@
+---
+title: Overview
+description: Learn where the UI package fits and how its explicit entry points support composition.
+order: 1
+toc:
+ - id: package-role
+ title: Package role
+ - id: tutorial-map
+ title: Tutorial map
+ - id: ui-or-blocks
+ title: UI or Blocks
+---
+
+## Package role {#package-role}
+
+`@workspace/ui` contains reusable React primitives, composite controls, hooks, icons, styles, and locale catalogs. It is the visual foundation shared by applications and higher-level feature packages.
+
+## Tutorial map {#tutorial-map}
+
+This tutorial installs the styles, introduces component subpaths, builds a small form through composition, adds responsive behavior, and connects locale catalogs.
+
+## UI or Blocks {#ui-or-blocks}
+
+Use UI components when the application owns the workflow. Use `@workspace/blocks` when the interface also needs a complete feature contract such as navigation, media storage, search adapters, or notification queries.
diff --git a/packages/ui/docs/zh-Hans/advanced/hooks.mdx b/packages/ui/docs/zh-Hans/advanced/hooks.mdx
new file mode 100644
index 0000000..611ad39
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/advanced/hooks.mdx
@@ -0,0 +1,31 @@
+---
+title: 添加响应式行为
+description: 当 CSS 无法表达行为变化时,使用共享的断点和交互 Hooks。
+order: 20
+toc:
+ - id: breakpoints
+ title: 断点
+ - id: server-rendering
+ title: 服务端渲染
+ - id: ripple
+ title: 波纹交互
+---
+
+## 断点 {#breakpoints}
+
+```tsx
+import { useBreakpoint, useIsMobile } from "@workspace/ui/hooks/use-breakpoint"
+
+const breakpoint = useBreakpoint()
+const isMobile = useIsMobile()
+```
+
+视觉变化优先使用 CSS 响应式变体。只有组件行为或挂载内容必须变化时才使用这些 Hooks。
+
+## 服务端渲染 {#server-rendering}
+
+断点 Store 使用确定的桌面端服务端快照。当应用需要 CSS 与行为断点在水合前保持一致时,可用 `getBreakpointInitializationScript` 提前设置匹配的根节点类名。
+
+## 波纹交互 {#ripple}
+
+`useRipple` 返回一个 ref 回调,为交互元素加入指针反馈。元素应创建定位上下文,使生成的覆盖层使用正确边界。
diff --git a/packages/ui/docs/zh-Hans/advanced/localization.mdx b/packages/ui/docs/zh-Hans/advanced/localization.mdx
new file mode 100644
index 0000000..9e1c41c
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/advanced/localization.mdx
@@ -0,0 +1,36 @@
+---
+title: UI 本地化
+description: 只加载当前语言所需的 UI 词典和日历 locale。
+order: 21
+toc:
+ - id: catalog-source
+ title: 词典来源
+ - id: direct-imports
+ title: 直接导入
+ - id: supported-locales
+ title: 支持的语言
+---
+
+## 词典来源 {#catalog-source}
+
+在应用国际化配置中加入 UI 词典来源:
+
+```json
+{
+ "catalogSources": ["@workspace/ui/locales/{locale}"]
+}
+```
+
+locales 根入口只包含元数据和类型,不会导入所有语言的词典。
+
+## 直接导入 {#direct-imports}
+
+```ts
+import { calendarLocale, messages } from "@workspace/ui/locales/zh-Hans"
+```
+
+每个语言入口还会导出对应的 `react-day-picker` locale。
+
+## 支持的语言 {#supported-locales}
+
+UI 词典支持简体中文(`zh-Hans`)和美式英语(`en-US`)。
diff --git a/packages/ui/docs/zh-Hans/examples/composition.mdx b/packages/ui/docs/zh-Hans/examples/composition.mdx
new file mode 100644
index 0000000..c89aa19
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/examples/composition.mdx
@@ -0,0 +1,37 @@
+---
+title: 组合表单
+description: 组合 Field、Input 和 Button,同时让领域状态继续由应用管理。
+order: 30
+toc:
+ - id: create-the-form
+ title: 创建表单
+ - id: validation
+ title: 表单验证
+ - id: styling
+ title: 样式
+---
+
+## 创建表单 {#create-the-form}
+
+```tsx
+import { Button } from "@workspace/ui/components/button"
+import { Field, FieldError, FieldLabel } from "@workspace/ui/components/field"
+import { Input } from "@workspace/ui/components/input"
+
+;
+```
+
+## 表单验证 {#validation}
+
+UI 包负责渲染验证状态,但不会指定表单库或 Schema。请在应用边界连接原生表单数据、React 状态或表单框架。
+
+## 样式 {#styling}
+
+受支持的语义变化使用组件变体,局部布局使用 `className`。需要合并条件类名或消费者覆盖时,使用 `@workspace/ui/lib/utils` 中的 `cn`。
diff --git a/packages/ui/docs/zh-Hans/guide/components.mdx b/packages/ui/docs/zh-Hans/guide/components.mdx
new file mode 100644
index 0000000..a73a399
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/guide/components.mdx
@@ -0,0 +1,30 @@
+---
+title: 选择组件
+description: 按职责选择基础组件,而不是导入一个庞大的统一组件入口。
+order: 11
+toc:
+ - id: forms
+ title: 表单与输入
+ - id: overlays
+ title: 浮层与菜单
+ - id: content
+ title: 内容与反馈
+ - id: icons
+ title: 图标
+---
+
+## 表单与输入 {#forms}
+
+普通表单可使用 `Button`、`Input`、`Textarea`、`Field` 和 `Label`。选择类输入可加入 `Checkbox`、`RadioGroup`、`Switch` 或 `Slider`;更丰富的选择场景可使用 `Select`、`Combobox`、`Command` 或 `Calendar`。
+
+## 浮层与菜单 {#overlays}
+
+`Dialog`、`AlertDialog`、`Sheet` 和 `Drawer` 用于模态界面。`Popover`、`HoverCard` 和 `Tooltip` 用于锚定式信息展示,菜单模块则覆盖上下文菜单、下拉菜单、菜单栏和导航模式。
+
+## 内容与反馈 {#content}
+
+卡片、列表项、表格、图表、进度条、骨架屏、加载器、空状态、消息和 Toast 构成通用展示词汇。Accordion、Collapsible、Tabs、Carousel 和 Pagination 用于组织更大规模的内容。
+
+## 图标 {#icons}
+
+从 `@workspace/ui/components/icon` 导入具名图标。该模块统一了 `@icones/react` 的图标词汇,使应用和各个包使用同一图标来源。
diff --git a/packages/ui/docs/zh-Hans/guide/setup.mdx b/packages/ui/docs/zh-Hans/guide/setup.mdx
new file mode 100644
index 0000000..e3ea91c
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/guide/setup.mdx
@@ -0,0 +1,39 @@
+---
+title: 项目配置
+description: 导入全局样式,并从显式组件子路径开始使用。
+order: 10
+toc:
+ - id: import-styles
+ title: 导入样式
+ - id: import-a-component
+ title: 导入组件
+ - id: entry-points
+ title: 包入口
+---
+
+## 导入样式 {#import-styles}
+
+在应用入口中导入一次全局样式:
+
+```ts
+import "@workspace/ui/globals.css"
+```
+
+它提供组件所需的共享 Tailwind 层、设计变量、字体配置和工具类。
+
+## 导入组件 {#import-a-component}
+
+```tsx
+import { Button } from "@workspace/ui/components/button"
+
+export function SaveButton() {
+ return
+}
+```
+
+## 包入口 {#entry-points}
+
+- `@workspace/ui/components/*` 导出单个组件模块。
+- `@workspace/ui/hooks/*` 导出可复用的交互和媒体查询状态。
+- `@workspace/ui/lib/utils` 导出共享的类名组合工具。
+- `@workspace/ui/locales/*` 导出指定语言的词典。
diff --git a/packages/ui/docs/zh-Hans/overview.mdx b/packages/ui/docs/zh-Hans/overview.mdx
new file mode 100644
index 0000000..edaea83
--- /dev/null
+++ b/packages/ui/docs/zh-Hans/overview.mdx
@@ -0,0 +1,24 @@
+---
+title: 概览
+description: 了解 UI 包的定位,以及显式入口如何支持组件组合。
+order: 1
+toc:
+ - id: package-role
+ title: 包的职责
+ - id: tutorial-map
+ title: 教程路线
+ - id: ui-or-blocks
+ title: UI 还是 Blocks
+---
+
+## 包的职责 {#package-role}
+
+`@workspace/ui` 包含可复用的 React 基础组件、复合控件、Hooks、图标、样式和本地化词典。它是应用与上层功能包共享的视觉基础。
+
+## 教程路线 {#tutorial-map}
+
+本教程会安装样式、介绍组件子路径、通过组合构建一个小型表单、添加响应式行为,并接入本地化词典。
+
+## UI 还是 Blocks {#ui-or-blocks}
+
+当应用自己拥有业务流程时使用 UI 组件。当界面还需要导航、媒体存储、搜索适配器或通知查询等完整功能契约时,使用 `@workspace/blocks`。