From dc80bd8ae0dac18513ecaac9dfc8df808be25d01 Mon Sep 17 00:00:00 2001 From: Maofeng Date: Sun, 20 Sep 2026 15:53:28 +0800 Subject: [PATCH] feat(docs): add bilingual package documentation site --- docs/.gitignore | 24 + docs/.oxlintrc.json | 8 + docs/README.md | 35 + docs/bun.lock | 255 +++++++ docs/index.html | 13 + docs/package.json | 42 ++ docs/public/favicon.svg | 1 + docs/public/icons.svg | 24 + docs/src/App.tsx | 145 ++++ docs/src/assets/hero.png | Bin 0 -> 13057 bytes docs/src/assets/react.svg | 1 + docs/src/assets/vite.svg | 1 + docs/src/components/article.tsx | 0 docs/src/components/docs-search-provider.tsx | 179 +++++ docs/src/components/language-switcher.tsx | 264 +++++++ docs/src/components/layout.tsx | 94 +++ docs/src/components/locale-flag.tsx | 17 + docs/src/components/locale-provider.tsx | 20 + docs/src/components/mdx.tsx | 93 +++ docs/src/components/preferences-provider.tsx | 228 ++++++ docs/src/components/search-trigger.tsx | 24 + docs/src/components/settings-link.tsx | 39 + docs/src/components/swatch.tsx | 48 ++ docs/src/components/theme-switch.tsx | 55 ++ docs/src/content/registry.ts | 355 +++++++++ docs/src/content/types.ts | 45 ++ docs/src/index.css | 86 +++ docs/src/lib/appearance.ts | 155 ++++ docs/src/lib/locale-navigation.ts | 14 + docs/src/lib/locale.ts | 32 + docs/src/lib/preferences.ts | 188 +++++ docs/src/lib/use-mobile.ts | 19 + docs/src/lib/utils.ts | 8 + docs/src/main.tsx | 18 + docs/src/pages/article.tsx | 704 ++++++++++++++++++ docs/src/pages/index.tsx | 49 ++ docs/src/pages/settings.tsx | 365 +++++++++ docs/src/satteri-modules.d.ts | 9 + docs/tsconfig.app.json | 26 + docs/tsconfig.json | 7 + docs/tsconfig.node.json | 23 + docs/vite.config.ts | 81 ++ package.json | 1 + .../blocks/docs/en-US/advanced/appearance.mdx | 40 + packages/blocks/docs/en-US/advanced/media.mdx | 34 + .../docs/en-US/advanced/notifications.mdx | 21 + packages/blocks/docs/en-US/examples/chats.mdx | 35 + .../docs/en-US/guide/application-shell.mdx | 45 ++ .../blocks/docs/en-US/guide/installation.mdx | 40 + .../blocks/docs/en-US/guide/navigation.mdx | 46 ++ packages/blocks/docs/en-US/overview.mdx | 33 + .../docs/zh-Hans/advanced/appearance.mdx | 40 + .../blocks/docs/zh-Hans/advanced/media.mdx | 34 + .../docs/zh-Hans/advanced/notifications.mdx | 21 + .../blocks/docs/zh-Hans/examples/chats.mdx | 35 + .../docs/zh-Hans/guide/application-shell.mdx | 45 ++ .../docs/zh-Hans/guide/installation.mdx | 40 + .../blocks/docs/zh-Hans/guide/navigation.mdx | 46 ++ packages/blocks/docs/zh-Hans/overview.mdx | 33 + .../i18n/docs/en-US/advanced/catalogs.mdx | 33 + packages/i18n/docs/en-US/advanced/cli.mdx | 44 ++ packages/i18n/docs/en-US/examples/devtool.mdx | 36 + packages/i18n/docs/en-US/guide/runtime.mdx | 44 ++ packages/i18n/docs/en-US/guide/setup.mdx | 48 ++ packages/i18n/docs/en-US/overview.mdx | 35 + .../i18n/docs/zh-Hans/advanced/catalogs.mdx | 33 + packages/i18n/docs/zh-Hans/advanced/cli.mdx | 44 ++ .../i18n/docs/zh-Hans/examples/devtool.mdx | 36 + packages/i18n/docs/zh-Hans/guide/runtime.mdx | 44 ++ packages/i18n/docs/zh-Hans/guide/setup.mdx | 48 ++ packages/i18n/docs/zh-Hans/overview.mdx | 35 + .../docs/en-US/advanced/custom-actions.mdx | 44 ++ .../docs/en-US/advanced/localization.mdx | 30 + .../lexical/docs/en-US/examples/media.mdx | 46 ++ packages/lexical/docs/en-US/guide/actions.mdx | 42 ++ packages/lexical/docs/en-US/guide/setup.mdx | 47 ++ packages/lexical/docs/en-US/overview.mdx | 26 + .../docs/zh-Hans/advanced/custom-actions.mdx | 44 ++ .../docs/zh-Hans/advanced/localization.mdx | 30 + .../lexical/docs/zh-Hans/examples/media.mdx | 46 ++ .../lexical/docs/zh-Hans/guide/actions.mdx | 42 ++ packages/lexical/docs/zh-Hans/guide/setup.mdx | 47 ++ packages/lexical/docs/zh-Hans/overview.mdx | 26 + .../docs/en-US/advanced/lifecycle.mdx | 57 ++ .../docs/en-US/examples/theme-example.mdx | 67 ++ .../preferences/docs/en-US/guide/setup.mdx | 74 ++ packages/preferences/docs/en-US/overview.mdx | 28 + .../docs/zh-Hans/advanced/lifecycle.mdx | 57 ++ .../docs/zh-Hans/examples/theme-example.mdx | 67 ++ .../preferences/docs/zh-Hans/guide/setup.mdx | 74 ++ .../preferences/docs/zh-Hans/overview.mdx | 28 + .../en-US/advanced/adapter-and-pagination.mdx | 42 ++ .../docs/en-US/examples/global-search.mdx | 58 ++ packages/search/docs/en-US/guide/setup.mdx | 47 ++ packages/search/docs/en-US/overview.mdx | 29 + .../advanced/adapter-and-pagination.mdx | 42 ++ .../docs/zh-Hans/examples/global-search.mdx | 58 ++ packages/search/docs/zh-Hans/guide/setup.mdx | 47 ++ packages/search/docs/zh-Hans/overview.mdx | 29 + packages/ui/docs/en-US/advanced/hooks.mdx | 31 + .../ui/docs/en-US/advanced/localization.mdx | 36 + .../ui/docs/en-US/examples/composition.mdx | 37 + packages/ui/docs/en-US/guide/components.mdx | 30 + packages/ui/docs/en-US/guide/setup.mdx | 39 + packages/ui/docs/en-US/overview.mdx | 24 + packages/ui/docs/zh-Hans/advanced/hooks.mdx | 31 + .../ui/docs/zh-Hans/advanced/localization.mdx | 36 + .../ui/docs/zh-Hans/examples/composition.mdx | 37 + packages/ui/docs/zh-Hans/guide/components.mdx | 30 + packages/ui/docs/zh-Hans/guide/setup.mdx | 39 + packages/ui/docs/zh-Hans/overview.mdx | 24 + 111 files changed, 6531 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/.oxlintrc.json create mode 100644 docs/README.md create mode 100644 docs/bun.lock create mode 100644 docs/index.html create mode 100644 docs/package.json create mode 100644 docs/public/favicon.svg create mode 100644 docs/public/icons.svg create mode 100644 docs/src/App.tsx create mode 100644 docs/src/assets/hero.png create mode 100644 docs/src/assets/react.svg create mode 100644 docs/src/assets/vite.svg create mode 100644 docs/src/components/article.tsx create mode 100644 docs/src/components/docs-search-provider.tsx create mode 100644 docs/src/components/language-switcher.tsx create mode 100644 docs/src/components/layout.tsx create mode 100644 docs/src/components/locale-flag.tsx create mode 100644 docs/src/components/locale-provider.tsx create mode 100644 docs/src/components/mdx.tsx create mode 100644 docs/src/components/preferences-provider.tsx create mode 100644 docs/src/components/search-trigger.tsx create mode 100644 docs/src/components/settings-link.tsx create mode 100644 docs/src/components/swatch.tsx create mode 100644 docs/src/components/theme-switch.tsx create mode 100644 docs/src/content/registry.ts create mode 100644 docs/src/content/types.ts create mode 100644 docs/src/index.css create mode 100644 docs/src/lib/appearance.ts create mode 100644 docs/src/lib/locale-navigation.ts create mode 100644 docs/src/lib/locale.ts create mode 100644 docs/src/lib/preferences.ts create mode 100644 docs/src/lib/use-mobile.ts create mode 100644 docs/src/lib/utils.ts create mode 100644 docs/src/main.tsx create mode 100644 docs/src/pages/article.tsx create mode 100644 docs/src/pages/index.tsx create mode 100644 docs/src/pages/settings.tsx create mode 100644 docs/src/satteri-modules.d.ts create mode 100644 docs/tsconfig.app.json create mode 100644 docs/tsconfig.json create mode 100644 docs/tsconfig.node.json create mode 100644 docs/vite.config.ts create mode 100644 packages/blocks/docs/en-US/advanced/appearance.mdx create mode 100644 packages/blocks/docs/en-US/advanced/media.mdx create mode 100644 packages/blocks/docs/en-US/advanced/notifications.mdx create mode 100644 packages/blocks/docs/en-US/examples/chats.mdx create mode 100644 packages/blocks/docs/en-US/guide/application-shell.mdx create mode 100644 packages/blocks/docs/en-US/guide/installation.mdx create mode 100644 packages/blocks/docs/en-US/guide/navigation.mdx create mode 100644 packages/blocks/docs/en-US/overview.mdx create mode 100644 packages/blocks/docs/zh-Hans/advanced/appearance.mdx create mode 100644 packages/blocks/docs/zh-Hans/advanced/media.mdx create mode 100644 packages/blocks/docs/zh-Hans/advanced/notifications.mdx create mode 100644 packages/blocks/docs/zh-Hans/examples/chats.mdx create mode 100644 packages/blocks/docs/zh-Hans/guide/application-shell.mdx create mode 100644 packages/blocks/docs/zh-Hans/guide/installation.mdx create mode 100644 packages/blocks/docs/zh-Hans/guide/navigation.mdx create mode 100644 packages/blocks/docs/zh-Hans/overview.mdx create mode 100644 packages/i18n/docs/en-US/advanced/catalogs.mdx create mode 100644 packages/i18n/docs/en-US/advanced/cli.mdx create mode 100644 packages/i18n/docs/en-US/examples/devtool.mdx create mode 100644 packages/i18n/docs/en-US/guide/runtime.mdx create mode 100644 packages/i18n/docs/en-US/guide/setup.mdx create mode 100644 packages/i18n/docs/en-US/overview.mdx create mode 100644 packages/i18n/docs/zh-Hans/advanced/catalogs.mdx create mode 100644 packages/i18n/docs/zh-Hans/advanced/cli.mdx create mode 100644 packages/i18n/docs/zh-Hans/examples/devtool.mdx create mode 100644 packages/i18n/docs/zh-Hans/guide/runtime.mdx create mode 100644 packages/i18n/docs/zh-Hans/guide/setup.mdx create mode 100644 packages/i18n/docs/zh-Hans/overview.mdx create mode 100644 packages/lexical/docs/en-US/advanced/custom-actions.mdx create mode 100644 packages/lexical/docs/en-US/advanced/localization.mdx create mode 100644 packages/lexical/docs/en-US/examples/media.mdx create mode 100644 packages/lexical/docs/en-US/guide/actions.mdx create mode 100644 packages/lexical/docs/en-US/guide/setup.mdx create mode 100644 packages/lexical/docs/en-US/overview.mdx create mode 100644 packages/lexical/docs/zh-Hans/advanced/custom-actions.mdx create mode 100644 packages/lexical/docs/zh-Hans/advanced/localization.mdx create mode 100644 packages/lexical/docs/zh-Hans/examples/media.mdx create mode 100644 packages/lexical/docs/zh-Hans/guide/actions.mdx create mode 100644 packages/lexical/docs/zh-Hans/guide/setup.mdx create mode 100644 packages/lexical/docs/zh-Hans/overview.mdx create mode 100644 packages/preferences/docs/en-US/advanced/lifecycle.mdx create mode 100644 packages/preferences/docs/en-US/examples/theme-example.mdx create mode 100644 packages/preferences/docs/en-US/guide/setup.mdx create mode 100644 packages/preferences/docs/en-US/overview.mdx create mode 100644 packages/preferences/docs/zh-Hans/advanced/lifecycle.mdx create mode 100644 packages/preferences/docs/zh-Hans/examples/theme-example.mdx create mode 100644 packages/preferences/docs/zh-Hans/guide/setup.mdx create mode 100644 packages/preferences/docs/zh-Hans/overview.mdx create mode 100644 packages/search/docs/en-US/advanced/adapter-and-pagination.mdx create mode 100644 packages/search/docs/en-US/examples/global-search.mdx create mode 100644 packages/search/docs/en-US/guide/setup.mdx create mode 100644 packages/search/docs/en-US/overview.mdx create mode 100644 packages/search/docs/zh-Hans/advanced/adapter-and-pagination.mdx create mode 100644 packages/search/docs/zh-Hans/examples/global-search.mdx create mode 100644 packages/search/docs/zh-Hans/guide/setup.mdx create mode 100644 packages/search/docs/zh-Hans/overview.mdx create mode 100644 packages/ui/docs/en-US/advanced/hooks.mdx create mode 100644 packages/ui/docs/en-US/advanced/localization.mdx create mode 100644 packages/ui/docs/en-US/examples/composition.mdx create mode 100644 packages/ui/docs/en-US/guide/components.mdx create mode 100644 packages/ui/docs/en-US/guide/setup.mdx create mode 100644 packages/ui/docs/en-US/overview.mdx create mode 100644 packages/ui/docs/zh-Hans/advanced/hooks.mdx create mode 100644 packages/ui/docs/zh-Hans/advanced/localization.mdx create mode 100644 packages/ui/docs/zh-Hans/examples/composition.mdx create mode 100644 packages/ui/docs/zh-Hans/guide/components.mdx create mode 100644 packages/ui/docs/zh-Hans/guide/setup.mdx create mode 100644 packages/ui/docs/zh-Hans/overview.mdx 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 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 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 @@ +Vite 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 = ( +
+

+ {t({ zh: "语言", us: "Language" })} +

+
    { + if ( + event.key !== "ArrowDown" && + event.key !== "ArrowUp" && + event.key !== "Home" && + event.key !== "End" + ) { + return + } + + event.preventDefault() + const items = getMenuItems(event.currentTarget) + const currentIndex = items.indexOf( + document.activeElement as HTMLButtonElement + ) + let nextIndex = currentIndex + + if (event.key === "Home") nextIndex = 0 + if (event.key === "End") nextIndex = items.length - 1 + if (event.key === "ArrowDown") { + nextIndex = (currentIndex + 1) % items.length + } + if (event.key === "ArrowUp") { + nextIndex = (currentIndex - 1 + items.length) % items.length + } + + items[nextIndex]?.focus() + }} + > + {localeOptions.map((option) => { + const selected = option.locale === locale + + return ( +
  • + selectLocale(option.locale)} + > + + {option.label} + {selected && ( + + )} + +
  • + ) + })} +
+
+ ) + + 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 ( +

+ {id ? ( + + {children} + + # + + + ) : ( + children + )} +

+ ) +} + +function MdxHeading3({ children, id, ...props }: React.ComponentProps<"h3">) { + return ( +

+ {id ? ( + + {children} + + # + + + ) : ( + children + )} +

+ ) +} + +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( +
+ , + document.body + )} + +
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + Copyright © 2026 + +
+ + ) +} + +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 ( +
+ +

{docLocation.page.title}

+

+ {docLocation.page.description} +

+
+ ) +} + +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 ( +
    + {previous && ( + + )} + {next && ( + + )} +
    + ) +} + +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} +

    +
    +
    + ))} +
    +
    + +
    Copyright © 2026
    +
    +
    + + ) +} 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)} +

    +
    +
    + {t(messages.theme)} +
    { + if ( + event.key !== "ArrowDown" && + event.key !== "ArrowUp" && + event.key !== "ArrowRight" && + event.key !== "ArrowLeft" && + event.key !== "Home" && + event.key !== "End" + ) { + return + } + + event.preventDefault() + const options = Array.from( + event.currentTarget.querySelectorAll( + '[role="radio"]' + ) + ) + const currentIndex = options.indexOf( + document.activeElement as HTMLButtonElement + ) + const moveForward = + event.key === "ArrowDown" || event.key === "ArrowRight" + let nextIndex = currentIndex + + if (event.key === "Home") nextIndex = 0 + else if (event.key === "End") nextIndex = options.length - 1 + else if (moveForward) { + nextIndex = (currentIndex + 1) % options.length + } else { + nextIndex = + (currentIndex - 1 + options.length) % options.length + } + + options[nextIndex]?.focus() + options[nextIndex]?.click() + }} + > + {themeModes.map((mode) => { + const selected = appearance.themeMode === mode + + return ( + + ) + })} +
    +
    + + ({ + 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.foreground)} +
    + {foregroundOptions.map((option) => ( + + appearance.updateAppearance({ + foregroundShade: option.value, + }) + } + /> + ))} +
    +
    +
    +
    + +
    +

    + {t(messages.language)} +

    +
    +
    + {t(messages.language)} +
    + selectLocale("zh-Hans")} + /> + selectLocale("en-US")} + /> +
    +
    + +
    +
    {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 ( +
    + {label} +
    + {options.map((option) => { + const colors = getColors(option) + const optionLabel = capitalize(option) + + return ( + onChange(option)} + /> + ) + })} +
    +
    + ) +} + +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" + +;
    + + Display name + + {errors.displayName} + + +
    +``` + +## 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" + +;
    + + 显示名称 + + {errors.displayName} + + +
    +``` + +## 表单验证 {#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`。