From 4bfb024516ccd3d1fd910d9a7e90e4afa83d3d7b Mon Sep 17 00:00:00 2001 From: moecasts Date: Sat, 20 Jul 2024 16:15:49 +0800 Subject: [PATCH] feat(site): add docs site --- site/.gitignore | 24 ++ site/CHANGELOG.md | 43 +++ site/README.md | 0 site/index.html | 13 + site/package.json | 139 ++++++++ site/packages/rd-theme/README.md | 1 + site/packages/rd-theme/package.json | 12 + .../packages/rd-theme/src/common/constants.ts | 1 + .../rd-theme/src/common/get-prefix-cls.ts | 12 + site/packages/rd-theme/src/common/index.ts | 2 + site/packages/rd-theme/src/components/app.tsx | 120 +++++++ .../src/components/doc-features/api.tsx | 104 ++++++ .../components/doc-features/doc-banner.tsx | 109 ++++++ .../src/components/doc-features/features.tsx | 34 ++ .../components/doc-features/float-button.tsx | 62 ++++ .../src/components/doc-features/hero.tsx | 80 +++++ .../rd-theme/src/components/hooks/index.ts | 4 + .../src/components/hooks/use-app-context.ts | 5 + .../src/components/hooks/use-app-locale.ts | 33 ++ .../src/components/hooks/use-aside.ts | 23 ++ .../src/components/hooks/use-theme-switch.ts | 21 ++ .../packages/rd-theme/src/components/index.ts | 1 + .../src/components/layout/component-doc.tsx | 17 + .../src/components/layout/content.tsx | 36 ++ .../rd-theme/src/components/layout/footer.tsx | 23 ++ .../rd-theme/src/components/layout/header.tsx | 151 ++++++++ .../src/components/layout/markdown.tsx | 103 ++++++ .../rd-theme/src/components/layout/page.tsx | 19 ++ .../rd-theme/src/components/layout/router.tsx | 39 +++ .../rd-theme/src/components/layout/routes.tsx | 33 ++ .../src/components/layout/sidebar.tsx | 24 ++ .../rd-theme/src/components/layout/toc.tsx | 34 ++ .../src/components/playground/code.scss | 76 +++++ .../src/components/playground/code.tsx | 119 +++++++ .../components/playground/externail-link.tsx | 18 + .../components/playground/use-code-sandbox.ts | 7 + .../rd-theme/src/components/store/context.tsx | 30 ++ .../src/components/store/provider.tsx | 37 ++ .../rd-theme/src/components/styles/_vars.scss | 6 + .../rd-theme/src/components/styles/app.scss | 300 ++++++++++++++++ .../src/components/styles/doc-banner.scss | 88 +++++ .../rd-theme/src/components/styles/page.scss | 141 ++++++++ site/packages/rd-theme/src/hooks/index.ts | 1 + site/packages/rd-theme/src/index.ts | 5 + site/packages/rd-theme/src/storage/theme.ts | 7 + site/packages/rd-theme/src/theme/index.ts | 32 ++ site/packages/rd-theme/src/types/index.ts | 2 + site/packages/rd-theme/src/types/locale.ts | 7 + site/packages/rd-theme/src/types/sidebar.ts | 6 + site/packages/rd-theme/src/utils/index.ts | 1 + .../rd-theme/src/utils/is-link-click.ts | 19 ++ site/packages/rd-vite/README.md | 1 + site/packages/rd-vite/docs/api-parser.md | 14 + site/packages/rd-vite/package.json | 12 + .../rd-vite/src/client/components/context.tsx | 54 +++ .../rd-vite/src/client/components/index.ts | 2 + .../components/non-leaf-route-outlet.tsx | 27 ++ .../rd-vite/src/client/hooks/index.ts | 2 + .../src/client/hooks/use-current-locale.ts | 28 ++ .../src/client/hooks/use-locale-location.ts | 62 ++++ .../rd-vite/src/client/hooks/use-rd.ts | 5 + site/packages/rd-vite/src/client/index.ts | 2 + .../rd-vite/src/client/utils/index.ts | 1 + .../rd-vite/src/client/utils/route.ts | 94 +++++ site/packages/rd-vite/src/common/index.ts | 1 + site/packages/rd-vite/src/common/locale.ts | 14 + site/packages/rd-vite/src/index.ts | 275 +++++++++++++++ site/packages/rd-vite/src/node/index.ts | 3 + .../rd-vite/src/node/provider/import.ts | 20 ++ .../rd-vite/src/node/provider/index.ts | 4 + .../rd-vite/src/node/provider/markdown.ts | 68 ++++ .../packages/rd-vite/src/node/provider/nav.ts | 43 +++ .../rd-vite/src/node/provider/route.ts | 117 +++++++ .../rd-vite/src/node/provider/sitemap.ts | 101 ++++++ .../rd-vite/src/node/provider/virtual.ts | 32 ++ .../rd-vite/src/node/rehypes/index.ts | 1 + site/packages/rd-vite/src/node/rehypes/toc.ts | 18 + .../src/node/remarks/code-block-replacer.ts | 19 ++ .../rd-vite/src/node/remarks/code-block.ts | 323 ++++++++++++++++++ .../rd-vite/src/node/remarks/index.ts | 3 + .../rd-vite/src/node/remarks/react-api.ts | 278 +++++++++++++++ site/packages/rd-vite/src/types/api.ts | 11 + site/packages/rd-vite/src/types/import.ts | 3 + site/packages/rd-vite/src/types/index.ts | 5 + site/packages/rd-vite/src/types/mdx.ts | 5 + site/packages/rd-vite/src/types/navigator.ts | 7 + site/packages/rd-vite/src/types/resolve.ts | 5 + site/packages/rd-vite/src/types/source.ts | 25 ++ .../src/utils/array-insert-interval.ts | 16 + site/packages/rd-vite/src/utils/index.ts | 2 + .../rd-vite/src/utils/order-routes.ts | 50 +++ site/packages/rd-vite/src/utils/path.ts | 52 +++ site/packages/rd-vite/src/utils/toc.ts | 104 ++++++ site/prerender.js | 66 ++++ site/public/favicon.svg | 6 + site/src/App.tsx | 11 + site/src/assets/favicon.svg | 6 + site/src/assets/home-banner.svg | 15 + site/src/assets/react.svg | 1 + site/src/brand.svg | 6 + site/src/entry-client.tsx | 22 ++ site/src/entry-server.tsx | 56 +++ site/src/main.tsx | 13 + site/src/vite-env.d.ts | 1 + site/tsconfig.json | 13 + site/tsconfig.node.json | 9 + site/typings.d.ts | 14 + site/typings/react-router-dom.d.ts | 14 + site/vite.config.ts | 59 ++++ 109 files changed, 4445 insertions(+) create mode 100644 site/.gitignore create mode 100644 site/CHANGELOG.md create mode 100644 site/README.md create mode 100644 site/index.html create mode 100644 site/package.json create mode 100644 site/packages/rd-theme/README.md create mode 100644 site/packages/rd-theme/package.json create mode 100644 site/packages/rd-theme/src/common/constants.ts create mode 100644 site/packages/rd-theme/src/common/get-prefix-cls.ts create mode 100644 site/packages/rd-theme/src/common/index.ts create mode 100644 site/packages/rd-theme/src/components/app.tsx create mode 100644 site/packages/rd-theme/src/components/doc-features/api.tsx create mode 100644 site/packages/rd-theme/src/components/doc-features/doc-banner.tsx create mode 100644 site/packages/rd-theme/src/components/doc-features/features.tsx create mode 100644 site/packages/rd-theme/src/components/doc-features/float-button.tsx create mode 100644 site/packages/rd-theme/src/components/doc-features/hero.tsx create mode 100644 site/packages/rd-theme/src/components/hooks/index.ts create mode 100644 site/packages/rd-theme/src/components/hooks/use-app-context.ts create mode 100644 site/packages/rd-theme/src/components/hooks/use-app-locale.ts create mode 100644 site/packages/rd-theme/src/components/hooks/use-aside.ts create mode 100644 site/packages/rd-theme/src/components/hooks/use-theme-switch.ts create mode 100644 site/packages/rd-theme/src/components/index.ts create mode 100644 site/packages/rd-theme/src/components/layout/component-doc.tsx create mode 100644 site/packages/rd-theme/src/components/layout/content.tsx create mode 100644 site/packages/rd-theme/src/components/layout/footer.tsx create mode 100644 site/packages/rd-theme/src/components/layout/header.tsx create mode 100644 site/packages/rd-theme/src/components/layout/markdown.tsx create mode 100644 site/packages/rd-theme/src/components/layout/page.tsx create mode 100644 site/packages/rd-theme/src/components/layout/router.tsx create mode 100644 site/packages/rd-theme/src/components/layout/routes.tsx create mode 100644 site/packages/rd-theme/src/components/layout/sidebar.tsx create mode 100644 site/packages/rd-theme/src/components/layout/toc.tsx create mode 100644 site/packages/rd-theme/src/components/playground/code.scss create mode 100644 site/packages/rd-theme/src/components/playground/code.tsx create mode 100644 site/packages/rd-theme/src/components/playground/externail-link.tsx create mode 100644 site/packages/rd-theme/src/components/playground/use-code-sandbox.ts create mode 100644 site/packages/rd-theme/src/components/store/context.tsx create mode 100644 site/packages/rd-theme/src/components/store/provider.tsx create mode 100644 site/packages/rd-theme/src/components/styles/_vars.scss create mode 100644 site/packages/rd-theme/src/components/styles/app.scss create mode 100644 site/packages/rd-theme/src/components/styles/doc-banner.scss create mode 100644 site/packages/rd-theme/src/components/styles/page.scss create mode 100644 site/packages/rd-theme/src/hooks/index.ts create mode 100644 site/packages/rd-theme/src/index.ts create mode 100644 site/packages/rd-theme/src/storage/theme.ts create mode 100644 site/packages/rd-theme/src/theme/index.ts create mode 100644 site/packages/rd-theme/src/types/index.ts create mode 100644 site/packages/rd-theme/src/types/locale.ts create mode 100644 site/packages/rd-theme/src/types/sidebar.ts create mode 100644 site/packages/rd-theme/src/utils/index.ts create mode 100644 site/packages/rd-theme/src/utils/is-link-click.ts create mode 100644 site/packages/rd-vite/README.md create mode 100644 site/packages/rd-vite/docs/api-parser.md create mode 100644 site/packages/rd-vite/package.json create mode 100644 site/packages/rd-vite/src/client/components/context.tsx create mode 100644 site/packages/rd-vite/src/client/components/index.ts create mode 100644 site/packages/rd-vite/src/client/components/non-leaf-route-outlet.tsx create mode 100644 site/packages/rd-vite/src/client/hooks/index.ts create mode 100644 site/packages/rd-vite/src/client/hooks/use-current-locale.ts create mode 100644 site/packages/rd-vite/src/client/hooks/use-locale-location.ts create mode 100644 site/packages/rd-vite/src/client/hooks/use-rd.ts create mode 100644 site/packages/rd-vite/src/client/index.ts create mode 100644 site/packages/rd-vite/src/client/utils/index.ts create mode 100644 site/packages/rd-vite/src/client/utils/route.ts create mode 100644 site/packages/rd-vite/src/common/index.ts create mode 100644 site/packages/rd-vite/src/common/locale.ts create mode 100644 site/packages/rd-vite/src/index.ts create mode 100644 site/packages/rd-vite/src/node/index.ts create mode 100644 site/packages/rd-vite/src/node/provider/import.ts create mode 100644 site/packages/rd-vite/src/node/provider/index.ts create mode 100644 site/packages/rd-vite/src/node/provider/markdown.ts create mode 100644 site/packages/rd-vite/src/node/provider/nav.ts create mode 100644 site/packages/rd-vite/src/node/provider/route.ts create mode 100644 site/packages/rd-vite/src/node/provider/sitemap.ts create mode 100644 site/packages/rd-vite/src/node/provider/virtual.ts create mode 100644 site/packages/rd-vite/src/node/rehypes/index.ts create mode 100644 site/packages/rd-vite/src/node/rehypes/toc.ts create mode 100644 site/packages/rd-vite/src/node/remarks/code-block-replacer.ts create mode 100644 site/packages/rd-vite/src/node/remarks/code-block.ts create mode 100644 site/packages/rd-vite/src/node/remarks/index.ts create mode 100644 site/packages/rd-vite/src/node/remarks/react-api.ts create mode 100644 site/packages/rd-vite/src/types/api.ts create mode 100644 site/packages/rd-vite/src/types/import.ts create mode 100644 site/packages/rd-vite/src/types/index.ts create mode 100644 site/packages/rd-vite/src/types/mdx.ts create mode 100644 site/packages/rd-vite/src/types/navigator.ts create mode 100644 site/packages/rd-vite/src/types/resolve.ts create mode 100644 site/packages/rd-vite/src/types/source.ts create mode 100644 site/packages/rd-vite/src/utils/array-insert-interval.ts create mode 100644 site/packages/rd-vite/src/utils/index.ts create mode 100644 site/packages/rd-vite/src/utils/order-routes.ts create mode 100644 site/packages/rd-vite/src/utils/path.ts create mode 100644 site/packages/rd-vite/src/utils/toc.ts create mode 100644 site/prerender.js create mode 100644 site/public/favicon.svg create mode 100644 site/src/App.tsx create mode 100644 site/src/assets/favicon.svg create mode 100644 site/src/assets/home-banner.svg create mode 100644 site/src/assets/react.svg create mode 100644 site/src/brand.svg create mode 100644 site/src/entry-client.tsx create mode 100644 site/src/entry-server.tsx create mode 100644 site/src/main.tsx create mode 100644 site/src/vite-env.d.ts create mode 100644 site/tsconfig.json create mode 100644 site/tsconfig.node.json create mode 100644 site/typings.d.ts create mode 100644 site/typings/react-router-dom.d.ts create mode 100644 site/vite.config.ts diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/site/.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/site/CHANGELOG.md b/site/CHANGELOG.md new file mode 100644 index 000000000..da35100a9 --- /dev/null +++ b/site/CHANGELOG.md @@ -0,0 +1,43 @@ +# site + +## 0.0.1 + +### Patch Changes + +- Updated dependencies + - @casts/portal@0.1.0 + - @casts/checkbox@0.1.0 + - @casts/theme@0.1.0 + - @casts/message@0.1.0 + - @casts/affix@0.1.0 + - @casts/toast@0.1.0 + - @casts/dialog@0.1.0 + - @casts/table@0.1.0 + - @casts/input@0.1.0 + - @casts/badge@0.1.0 + - @casts/layout@0.1.0 + - @casts/config-provider@0.1.0 + - @casts/space@0.1.0 + - @casts/form@0.1.0 + - @casts/notification@0.1.0 + - @casts/typography@0.1.0 + - @casts/grid@0.1.0 + - @casts/menu@0.1.0 + - @casts/progress@0.1.0 + - @casts/divider@0.1.0 + - @casts/standard@0.1.0 + - @casts/icons@0.1.0 + - @casts/avatar@0.1.0 + - @casts/button@0.1.0 + - @casts/code@0.1.0 + - @casts/alert@0.1.0 + - @casts/tooltip@0.1.0 + - @casts/anchor@0.1.0 + - @casts/popup@0.1.0 + - @casts/radio@0.1.0 + - @casts/locale@0.1.0 + - @casts/link@0.1.0 + - @casts/tabs@0.1.0 + - @casts/theme-generator@0.1.0 + - @casts/switch@0.1.0 + - @casts/common@0.1.0 diff --git a/site/README.md b/site/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/site/index.html b/site/index.html new file mode 100644 index 000000000..0fa35a303 --- /dev/null +++ b/site/index.html @@ -0,0 +1,13 @@ + + + + + + + <!--app-name --< + + +
+ + + diff --git a/site/package.json b/site/package.json new file mode 100644 index 000000000..da1a2115f --- /dev/null +++ b/site/package.json @@ -0,0 +1,139 @@ +{ + "name": "site", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build:client": "vite build --manifest --ssrManifest", + "build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx", + "build:ssg": "tsc && vite build && vite build --outDir dist/server --ssr src/entry-server.tsx && node ./prerender.js && rm -rf dist/server", + "preview": "vite preview" + }, + "dependencies": { + "@casts/affix": "workspace:^0.0.1", + "@casts/alert": "workspace:^0.0.1", + "@casts/anchor": "workspace:^0.0.1", + "@casts/avatar": "workspace:^0.0.1", + "@casts/badge": "workspace:^0.0.1", + "@casts/breadcrumbs": "workspace:^0.0.1", + "@casts/button": "workspace:^0.0.1", + "@casts/checkbox": "workspace:^0.0.1", + "@casts/code": "workspace:^0.0.1", + "@casts/common": "workspace:^0.0.1", + "@casts/config-provider": "workspace:^0.0.1", + "@casts/dialog": "workspace:^0.0.1", + "@casts/divider": "workspace:^0.0.1", + "@casts/empty": "workspace:^0.0.1", + "@casts/form": "workspace:^0.0.1", + "@casts/grid": "workspace:^0.0.1", + "@casts/icons": "workspace:^0.0.1", + "@casts/input": "workspace:^0.0.1", + "@casts/layout": "workspace:^0.0.1", + "@casts/link": "workspace:^0.0.1", + "@casts/locale": "workspace:^0.0.1", + "@casts/menu": "workspace:^0.0.1", + "@casts/message": "workspace:^0.0.1", + "@casts/notification": "workspace:^0.0.1", + "@casts/popup": "workspace:^0.0.1", + "@casts/portal": "workspace:^0.0.1", + "@casts/progress": "workspace:^0.0.1", + "@casts/radio": "workspace:^0.0.1", + "@casts/space": "workspace:^0.0.1", + "@casts/standard": "workspace:^0.0.1", + "@casts/switch": "workspace:^0.0.1", + "@casts/table": "workspace:^0.0.1", + "@casts/tabs": "workspace:^0.0.1", + "@casts/theme": "workspace:^0.0.1", + "@casts/theme-generator": "workspace:^0.0.1", + "@casts/toast": "workspace:^0.0.1", + "@casts/tooltip": "workspace:^0.0.1", + "@casts/typography": "workspace:^0.0.1", + "@floating-ui/react": "^0.26.4", + "@jsdevtools/rehype-toc": "^3.0.2", + "@juggle/resize-observer": "^3.4.0", + "@mdx-js/mdx": "^2.3.0", + "@mdx-js/react": "^2.3.0", + "@mdx-js/rollup": "^2.3.0", + "@tanstack/react-table": "^8.9.3", + "@theme-toggles/react": "^4.1.0", + "@vitejs/plugin-react": "^4.0.4", + "add": "^2.0.6", + "ahooks": "^3.7.8", + "clsx": "^2.0.0", + "copy-to-clipboard": "^3.3.3", + "estree-jsx": "^0.0.1", + "father": "4.3.2", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-toc": "^7.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-mdx-jsx": "^2.0.0", + "prism-react-renderer": "^2.0.6", + "prismjs": "^1.29.0", + "react": "^18.2.0", + "react-docgen-typescript": "^2.2.2", + "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", + "react-helmet-async": "^2.0.5", + "react-hook-form": "^7.45.4", + "react-hot-toast": "^2.4.1", + "react-is": "^18.2.0", + "react-router-dom": "^6.15.0", + "react-transition-group": "^4.4.5", + "rehype-autolink-headings": "^6.1.1", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "remark-comment": "^1.0.0", + "remark-frontmatter": "^4.0.1", + "remark-gfm": "^3.0.1", + "remark-mdx-frontmatter": "^3.0.0", + "remark-mdx-images": "^2.0.0", + "remove": "^0.1.5", + "resize-observer-polyfill": "^1.5.1", + "ripplet.js": "^1.1.0", + "to-vfile": "^7.2.4", + "validator": "^13.11.0", + "vfile-matter": "^4.0.1" + }, + "devDependencies": { + "@babel/core": "^7.22.11", + "@babel/parser": "^7.22.14", + "@babel/traverse": "^7.22.11", + "@babel/types": "^7.22.11", + "@swc/core": "^1.3.82", + "@types/babel__core": "^7.20.1", + "@types/dom-view-transitions": "^1.0.4", + "@types/estree": "^1.0.1", + "@types/hast": "^3.0.0", + "@types/lodash-es": "^4.17.9", + "@types/mdast": "^4.0.0", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@types/react-helmet": "^6.1.11", + "@types/react-transition-group": "^4.4.6", + "@types/unist": "^3.0.0", + "@vitejs/plugin-react-swc": "^3.3.2", + "@vitest/coverage-v8": "^1.3.1", + "acorn": "^8.10.0", + "autoprefixer": "^10.4.19", + "esbuild": "^0.19.2", + "import-meta-resolve": "^3.0.0", + "jsdom": "^22.1.0", + "mdast": "^3.0.0", + "postcss": "^8.4.38", + "postcss-discard-duplicates": "^6.0.3", + "rollup": "^3.28.1", + "sass": "^1.66.1", + "source-map": "^0.6.1", + "typescript": "5.5.1-rc", + "unified": "^10.1.2", + "unist-util-visit": "^5.0.0", + "vite": "^4.4.9", + "vite-plugin-dynamic-import": "^1.5.0", + "vite-plugin-svgr": "^3.2.0" + } +} diff --git a/site/packages/rd-theme/README.md b/site/packages/rd-theme/README.md new file mode 100644 index 000000000..d8457693c --- /dev/null +++ b/site/packages/rd-theme/README.md @@ -0,0 +1 @@ +# @casts/rd-theme diff --git a/site/packages/rd-theme/package.json b/site/packages/rd-theme/package.json new file mode 100644 index 000000000..01b5feddd --- /dev/null +++ b/site/packages/rd-theme/package.json @@ -0,0 +1,12 @@ +{ + "name": "@casts/rd-theme", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "moecasts", + "license": "ISC" +} \ No newline at end of file diff --git a/site/packages/rd-theme/src/common/constants.ts b/site/packages/rd-theme/src/common/constants.ts new file mode 100644 index 000000000..66ca82a43 --- /dev/null +++ b/site/packages/rd-theme/src/common/constants.ts @@ -0,0 +1 @@ +export const prefixCls = 'rd'; diff --git a/site/packages/rd-theme/src/common/get-prefix-cls.ts b/site/packages/rd-theme/src/common/get-prefix-cls.ts new file mode 100644 index 000000000..a9795597c --- /dev/null +++ b/site/packages/rd-theme/src/common/get-prefix-cls.ts @@ -0,0 +1,12 @@ +import { prefixCls } from './constants'; + +export const getCompletePrefixCls = ({ + prefixCls, + suffixCls, +}: { + prefixCls?: string; + suffixCls?: string; +}) => [prefixCls, suffixCls].filter((text) => !!text).join('-'); + +export const getPrefixCls = (suffixCls?: string) => + getCompletePrefixCls({ prefixCls, suffixCls }); diff --git a/site/packages/rd-theme/src/common/index.ts b/site/packages/rd-theme/src/common/index.ts new file mode 100644 index 000000000..beedaee15 --- /dev/null +++ b/site/packages/rd-theme/src/common/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './get-prefix-cls'; diff --git a/site/packages/rd-theme/src/components/app.tsx b/site/packages/rd-theme/src/components/app.tsx new file mode 100644 index 000000000..08d1dcbfa --- /dev/null +++ b/site/packages/rd-theme/src/components/app.tsx @@ -0,0 +1,120 @@ +import { FC, useEffect } from 'react'; +import { useRef } from 'react'; +import { isEmpty, scrollTo } from '@casts/common'; +import { + ConfigProvider, + type ConfigProviderProps, +} from '@casts/config-provider'; +import { Layout } from '@casts/layout'; +import { useRd } from '@casts/rd-vite/client/hooks/use-rd'; +import { CdsMotionDurationRapid } from '@casts/theme'; +import clsx from 'clsx'; +import { useNavigate } from 'react-router-dom'; +import { CSSTransition } from 'react-transition-group'; + +import { getPrefixCls } from '../common'; +import { FloatButton } from './doc-features/float-button'; +import { useAppLocale } from './hooks'; +import { SiteFooter } from './layout/footer'; +import { Header } from './layout/header'; +import { Router } from './layout/router'; +import { Sidebar } from './layout/sidebar'; +import { useAppContext } from './store/context'; +import { AppProvider } from './store/provider'; + +import '@casts/theme/styles/scss/core.scss'; +import './styles/app.scss'; + +const { Content, Footer, Aside } = Layout; + +export const App: FC> = (props) => { + return ( + + <_App {...props} /> + + ); +}; + +const _App: FC> = () => { + const { menu, matches, name } = useRd(); + + const asideContentRef = useRef(null); + const asideOverlayRef = useRef(null); + + const { themeMode, asideVisible, toggleAsideVisible } = useAppContext(); + + const navigate = useNavigate(); + + /** + * scroll to top when route change + */ + const currentRoute = matches?.[matches.length - 1].route; + useEffect(() => { + scrollTo(0); + }, [currentRoute]); + + useEffect(() => { + const title = [currentRoute?.meta?.title, name].filter(Boolean).join(' - '); + document.title = title; + }, [name, currentRoute?.meta?.title]); + + const { locale } = useAppLocale(); + + return ( + + +
+ + {!isEmpty(menu) && ( + + )} + + + + +
+ +
+
+
+ + + + ); +}; diff --git a/site/packages/rd-theme/src/components/doc-features/api.tsx b/site/packages/rd-theme/src/components/doc-features/api.tsx new file mode 100644 index 000000000..fac72c750 --- /dev/null +++ b/site/packages/rd-theme/src/components/doc-features/api.tsx @@ -0,0 +1,104 @@ +import { FC } from 'react'; +import { useMemo } from 'react'; +import { arrayInsertInterval, BaseComponentProps, map } from '@casts/common'; +import { + ApiDeclaration, + ApiDeclarations, + ApisDeclarations, +} from '@casts/rd-vite/types'; +import { Column, Table } from '@casts/table'; +import { Text } from '@casts/typography'; + +export type ApiProps = BaseComponentProps & { + apis: ApisDeclarations; +}; + +export const Api: FC = (props) => { + const { apis } = props; + + const columns = useMemo( + () => [ + { + key: 'property', + title: 'Property', + }, + { + key: 'description', + title: 'Description', + }, + { + key: 'type', + title: 'Type', + cell: ({ cell }) => { + const node = cell + .getValue() + .split(/\s*\|\s*/g) + .map((item: string) => { + const text = item.replace(/^(['"])(.*)\1$/, '$2'); + return ( + + {text} + + ); + }); + + return arrayInsertInterval(node, ' | ', 1); + }, + }, + { + key: 'default', + title: 'Default', + cell: ({ cell }) => { + const text = cell.getValue(); + if (!text) { + return '-'; + } + + return {text}; + }, + }, + { + key: 'required', + title: 'Required', + }, + ], + [], + ); + + const renderApis = (apis: ApisDeclarations) => { + return map( + map(apis, (declarations: ApiDeclarations, name: string) => { + const getDeclarationType = (type: ApiDeclaration['type']) => { + if (type.name !== 'enum') { + return type.name; + } + + return type.raw; + }; + + const getData = (declarations: ApiDeclarations) => + declarations.map((declaration) => ({ + property: declaration.identifier, + description: declaration.description || '', + type: getDeclarationType(declaration.type), + default: declaration.default, + required: declaration.required ? 'Y' : 'N', + })); + + return ( +
+

{name}

+ + + ); + }), + ); + }; + + return
{renderApis(apis)}
; +}; diff --git a/site/packages/rd-theme/src/components/doc-features/doc-banner.tsx b/site/packages/rd-theme/src/components/doc-features/doc-banner.tsx new file mode 100644 index 000000000..f11cfc230 --- /dev/null +++ b/site/packages/rd-theme/src/components/doc-features/doc-banner.tsx @@ -0,0 +1,109 @@ +import { useRef } from 'react'; +import { Breadcrumbs } from '@casts/breadcrumbs'; +import { identity, isEmpty, useScroll } from '@casts/common'; +import { useRd } from '@casts/rd-vite/client'; +import { Heading } from '@casts/typography'; +import clsx from 'clsx'; + +import { getPrefixCls } from '../../common'; + +import '../styles/doc-banner.scss'; + +export const DocBanner = () => { + const { matches } = useRd(); + + const currentRoute = matches?.[matches.length - 1].route; + + const prefixCls = getPrefixCls('doc-banner'); + + const meta = currentRoute?.meta; + + const scroll = useScroll(); + + const containerRef = useRef(null); + + const getStickyBottomOffsetTop = (element: HTMLElement | null) => { + if (!element) { + return; + } + + const rect = element.getBoundingClientRect(); + + // 获取元素的顶部偏移量 + const top = rect.top; + + // 获取元素的高度 + const height = rect.height; + + // 获取文档的滚动顶部偏移量 + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + + // 计算元素底边距离滚动顶部的距离 + // 元素的顶部偏移量 + 元素的高度 - 当前滚动的顶部偏移量 + return top + height + scrollTop; + }; + + const stickyOffsetTop = + (getStickyBottomOffsetTop(containerRef.current) || 0) - 76 - 64; + + const isFixed = + stickyOffsetTop > 0 && scroll?.top && scroll.top > stickyOffsetTop; + + const classes = clsx(`${prefixCls}-container`, { + [`${prefixCls}-container--fixed`]: isFixed, + }); + + const stickyClasses = clsx( + `${prefixCls}-container`, + `${prefixCls}-container--sticky`, + { + [`${prefixCls}-container--fixed`]: isFixed, + }, + ); + + const path = [ + meta?.nav && { + label: meta.nav?.title, + href: meta.nav?.path, + }, + meta?.group && { + label: meta.group?.title, + href: meta.group?.path, + }, + meta?.title && { + label: meta?.title, + }, + ].filter(identity); + + if (!meta?.title) { + return null; + } + + return ( + <> +
+
+ {!isEmpty(path) && ( + + )} + + {meta.title} + + {meta.description && ( +

{meta.description}

+ )} +
+
+
+
+ + {meta.title} + +
+
+ + ); +}; diff --git a/site/packages/rd-theme/src/components/doc-features/features.tsx b/site/packages/rd-theme/src/components/doc-features/features.tsx new file mode 100644 index 000000000..30f7f72b6 --- /dev/null +++ b/site/packages/rd-theme/src/components/doc-features/features.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { BaseComponentProps } from '@casts/common'; +import { useRd } from '@casts/rd-vite/client'; + +import { prefixCls } from '../../common/constants'; + +export const Features: FC = () => { + const { matches } = useRd(); + + const currentRoute = matches?.[matches.length - 1].route; + + const meta = (currentRoute as any)?.meta || {}; + + if (!meta?.features) { + return null; + } + + return ( +
+ {/** @ts-ignore custom route */} + {meta.features.map((feature) => ( +
+
{feature.icon}
+
+ {feature.title} +
+
+ {feature.desc} +
+
+ ))} +
+ ); +}; diff --git a/site/packages/rd-theme/src/components/doc-features/float-button.tsx b/site/packages/rd-theme/src/components/doc-features/float-button.tsx new file mode 100644 index 000000000..c091f822f --- /dev/null +++ b/site/packages/rd-theme/src/components/doc-features/float-button.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { Button } from '@casts/button'; +import { BrushLine } from '@casts/icons'; +import { MainColor, ThemeGenerator, ThemeMode } from '@casts/theme-generator'; + +import { getPrefixCls } from '../../common'; +import { themeStorage } from '../../storage/theme'; +import { defaultMainColors } from '../../theme'; + +export const FloatButton = () => { + const prefixCls = getPrefixCls('float-button'); + + const classes = `${prefixCls}`; + + const [themeGeneratorVisible, setThemeGeneratorVisible] = useState(false); + + const [themeMode, setThemeMode] = useState(() => { + return themeStorage.get()?.mode || 'default'; + }); + + const [themeMainColors, setThemeMainColors] = useState(() => { + return themeStorage.get()?.mainColors || defaultMainColors; + }); + + const updateTheme = (payload: { + mainColors?: MainColor[]; + mode?: ThemeMode; + }) => { + const { mainColors, mode } = payload; + + mainColors && setThemeMainColors(mainColors); + mode && setThemeMode(mode); + + // const theme = { + // mode: themeMode, + // mainColors: themeMainColors, + // ...payload, + // }; + // themeStorage.set(theme); + }; + + return ( + <> + + updateTheme({ mainColors })} + onModeChange={(mode) => updateTheme({ mode })} + visible={themeGeneratorVisible} + onVisibleChange={(visible) => setThemeGeneratorVisible(visible)} + // addThemeCodeOnMounted={!!themeStorage.get()} + /> + + ); +}; diff --git a/site/packages/rd-theme/src/components/doc-features/hero.tsx b/site/packages/rd-theme/src/components/doc-features/hero.tsx new file mode 100644 index 000000000..57b67324e --- /dev/null +++ b/site/packages/rd-theme/src/components/doc-features/hero.tsx @@ -0,0 +1,80 @@ +import { FC, useCallback } from 'react'; +import { Button } from '@casts/button'; +import { BaseComponentProps } from '@casts/common'; +import { Col, Row } from '@casts/grid'; +import { useRd } from '@casts/rd-vite/client'; +import { fallbackLocaleCode } from '@casts/rd-vite/common'; +import { Space } from '@casts/space'; + +// @ts-ignore svgr component +import { ReactComponent as Favicon } from '../../../../../src/assets/favicon.svg'; +// @ts-ignore svgr component +import { ReactComponent as HomeBanner } from '../../../../../src/assets/home-banner.svg'; +import { prefixCls } from '../../common/constants'; + +export const Hero: FC = () => { + const { matches, localeCode } = useRd(); + + const currentRoute = matches?.[matches.length - 1].route; + + const meta = currentRoute?.meta; + + const getLocaleLink = useCallback( + (link: string | undefined) => { + if (!link) { + return; + } + + if (localeCode === fallbackLocaleCode) { + return link; + } + + return `/${localeCode}${link}`; + }, + [localeCode], + ); + + if (!meta?.hero) { + return null; + } + + return ( + +
+ +
+
+

{meta.hero.title}

+

{meta.hero.desc}

+ {meta.hero.actions && ( + + {meta.hero.actions.map((action: any) => ( + + ))} + + )} + + +
+ +
+ + + ); +}; diff --git a/site/packages/rd-theme/src/components/hooks/index.ts b/site/packages/rd-theme/src/components/hooks/index.ts new file mode 100644 index 000000000..5cc0536a1 --- /dev/null +++ b/site/packages/rd-theme/src/components/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './use-app-context'; +export * from './use-app-locale'; +export * from './use-aside'; +export * from './use-theme-switch'; diff --git a/site/packages/rd-theme/src/components/hooks/use-app-context.ts b/site/packages/rd-theme/src/components/hooks/use-app-context.ts new file mode 100644 index 000000000..53f859c2e --- /dev/null +++ b/site/packages/rd-theme/src/components/hooks/use-app-context.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { AppContext } from '../store/context'; + +export const useAppContext = () => useContext(AppContext); diff --git a/site/packages/rd-theme/src/components/hooks/use-app-locale.ts b/site/packages/rd-theme/src/components/hooks/use-app-locale.ts new file mode 100644 index 000000000..ad80dc14f --- /dev/null +++ b/site/packages/rd-theme/src/components/hooks/use-app-locale.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import * as Locale from '@casts/locale'; +import { useRd } from '@casts/rd-vite/client/hooks/use-rd'; + +function convertLocaleFormat(locale: string): string { + return locale.replace(/-/g, ''); +} + +export const useAppLocale = () => { + const { localeCode } = useRd(); + + const [locale, setLocale] = useState(Locale.enUS); + + useEffect(() => { + let locale = Locale.enUS; + + if (localeCode) { + const localeCodeInFormat = convertLocaleFormat(localeCode); + + const newLocale = Locale[localeCodeInFormat as keyof typeof Locale]; + + if (newLocale) { + locale = newLocale as any; + } + } + + setLocale(locale); + }, [localeCode]); + + return { + locale, + }; +}; diff --git a/site/packages/rd-theme/src/components/hooks/use-aside.ts b/site/packages/rd-theme/src/components/hooks/use-aside.ts new file mode 100644 index 000000000..fb5947fe1 --- /dev/null +++ b/site/packages/rd-theme/src/components/hooks/use-aside.ts @@ -0,0 +1,23 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useAdaptive } from '@casts/common'; + +export const useAside = () => { + const adaptive = useAdaptive(); + + const isAsideShouldFloat = useMemo(() => !adaptive?.medium, [adaptive]); + + const [asideVisible, setAsideVisible] = useState(() => adaptive?.medium); + + const toggleAsideVisible = () => setAsideVisible(!asideVisible); + + useEffect(() => { + // always show aside when screen is bigger than medium + setAsideVisible(!isAsideShouldFloat); + }, [adaptive?.medium, isAsideShouldFloat]); + + return { + isAsideShouldFloat, + asideVisible, + toggleAsideVisible, + }; +}; diff --git a/site/packages/rd-theme/src/components/hooks/use-theme-switch.ts b/site/packages/rd-theme/src/components/hooks/use-theme-switch.ts new file mode 100644 index 000000000..4ef9e6f8f --- /dev/null +++ b/site/packages/rd-theme/src/components/hooks/use-theme-switch.ts @@ -0,0 +1,21 @@ +import { RefObject } from 'react'; +import { useCircleTransition } from '@casts/common'; + +import { useAppContext } from '../store/context'; + +export const useThemeSwitch = (themeSwitchRef: RefObject) => { + const { themeMode, setAppContext } = useAppContext(); + + const { circleTransition } = useCircleTransition(); + + const update = () => { + const newThemeMode = themeMode === 'dark' ? 'default' : 'dark'; + setAppContext({ themeMode: newThemeMode }); + }; + + const toggleThemeMode = async () => { + circleTransition({ ref: themeSwitchRef, update }); + }; + + return { toggleThemeMode }; +}; diff --git a/site/packages/rd-theme/src/components/index.ts b/site/packages/rd-theme/src/components/index.ts new file mode 100644 index 000000000..665a3d9d3 --- /dev/null +++ b/site/packages/rd-theme/src/components/index.ts @@ -0,0 +1 @@ +export * from './app'; diff --git a/site/packages/rd-theme/src/components/layout/component-doc.tsx b/site/packages/rd-theme/src/components/layout/component-doc.tsx new file mode 100644 index 000000000..11fd0fc37 --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/component-doc.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import { BaseComponentProps } from '@casts/common'; + +import { DocBanner } from '../doc-features/doc-banner'; +import { SiteContent } from './content'; + +export const ComponentDoc: FC = (props) => { + // eslint-disable-next-line no-empty-pattern + const {} = props; + + return ( + <> + + + + ); +}; diff --git a/site/packages/rd-theme/src/components/layout/content.tsx b/site/packages/rd-theme/src/components/layout/content.tsx new file mode 100644 index 000000000..d9cc8e1eb --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/content.tsx @@ -0,0 +1,36 @@ +import { Suspense, useRef } from 'react'; +import { CircularProgress } from '@casts/progress'; +import { MDXProvider } from '@mdx-js/react'; + +import { getPrefixCls } from '../../common'; +import { components } from './markdown'; +import { Routes } from './routes'; + +export const SiteContent = () => { + const containerRef = useRef(null); + + return ( +
+
+ + +
+ } + > + + + + +
+ + ); +}; diff --git a/site/packages/rd-theme/src/components/layout/footer.tsx b/site/packages/rd-theme/src/components/layout/footer.tsx new file mode 100644 index 000000000..101237aaf --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/footer.tsx @@ -0,0 +1,23 @@ +// create a site footer component + +import { FC } from 'react'; + +import { getPrefixCls } from '../../common'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type SiteFooterProps = {}; + +export const SiteFooter: FC = () => { + const currentYear = new Date().getFullYear(); + + return ( +
+
+

Open-source MIT Licensed | Copyright © {currentYear}

+

+ Powered by Casts Design +

+
+
+ ); +}; diff --git a/site/packages/rd-theme/src/components/layout/header.tsx b/site/packages/rd-theme/src/components/layout/header.tsx new file mode 100644 index 000000000..e07ae2d7c --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/header.tsx @@ -0,0 +1,151 @@ +import { FC, useMemo, useRef } from 'react'; +import { Button } from '@casts/button'; +import { isEmpty, last, map } from '@casts/common'; +import { MenuFoldLine, MenuUnfoldLine, Translate2 } from '@casts/icons'; +import { Layout } from '@casts/layout'; +import { HeadMenu } from '@casts/menu'; +import { useLocaleLocation } from '@casts/rd-vite/client/hooks/use-locale-location'; +import { useRd } from '@casts/rd-vite/client/hooks/use-rd'; +import { localeCodes } from '@casts/rd-vite/common'; +import { TokenCdsColorTextPrimary } from '@casts/theme'; +import clsx from 'clsx'; +import { Link, useNavigate } from 'react-router-dom'; + +// @ts-ignore svgr component +import { ReactComponent as Brand } from '../../../../../src/brand.svg'; +import { getPrefixCls, prefixCls } from '../../common'; +import { useAppContext } from '../hooks/use-app-context'; +import { useThemeSwitch } from '../hooks/use-theme-switch'; + +import '@theme-toggles/react/css/Around.css'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type HeaderProps = {}; + +const navToMenuData = (data: any): any[] => { + return map(data, (item) => { + const { title, path, ...rest } = item; + + const children = item.children ? navToMenuData(item.children) : undefined; + + return { + key: title, + label: title, + value: path, + children, + href: path, + ...rest, + }; + }); +}; + +export const Header: FC = () => { + const { nav = [], matches, menu } = useRd(); + const navigate = useNavigate(); + + const active = useMemo( + () => last(matches)?.route.meta?.nav?.path, + // eslint-disable-next-line react-hooks/exhaustive-deps + [last(matches)?.route.meta?.nav?.path], + ); + + const { firstDiffLocale, getLocaleLocation } = useLocaleLocation(localeCodes); + + const firstDiffLocaleLocation = useMemo( + () => getLocaleLocation(firstDiffLocale), + [firstDiffLocale, getLocaleLocation], + ); + + const indexRoute = useMemo(() => { + const baseRoute = matches?.[0]?.pathname || ''; + return `${baseRoute}/`.replace(/\/(\/)+/g, '/'); + }, [matches]); + + const themeSwitchRef = useRef(null); + + const { toggleThemeMode } = useThemeSwitch(themeSwitchRef); + + const { themeMode, isAsideShouldFloat, asideVisible, toggleAsideVisible } = + useAppContext(); + + return ( + + : } + onClick={toggleAsideVisible} + variant="text" + theme="neutral" + > + ) + } + logo={ + { + navigate(indexRoute); + }} + className={`${prefixCls}-logo`} + > + + + } + value={active} + size="large" + items={navToMenuData(nav)} + operations={ + <> + + + + + } + > + + ); +}; diff --git a/site/packages/rd-theme/src/components/layout/markdown.tsx b/site/packages/rd-theme/src/components/layout/markdown.tsx new file mode 100644 index 000000000..9d8cdbf1b --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/markdown.tsx @@ -0,0 +1,103 @@ +import { FC, ReactElement, ReactNode } from 'react'; +import { Code as BaseCode } from '@casts/code'; +import { BaseComponentProps } from '@casts/common'; +import { useConfig } from '@casts/config-provider'; +import { Heading, HeadingProps, Text } from '@casts/typography'; +import { Components } from '@mdx-js/react/lib'; +import clsx from 'clsx'; + +import { Api } from '../doc-features/api'; +import { Code } from '../playground/code'; +import { Toc } from './toc'; + +export const getHeadingComponents = (): Components => { + const levels = 4; + + return Array.from({ length: levels }).reduce( + (acc: Components, _, idx: number) => ({ + ...acc, + [`h${idx + 1}`]: ({ children, ...rest }: { children: ReactNode }) => { + return ( + + {children} + + ); + }, + }), + {}, + ); +}; + +export const Table: FC = (props) => { + const { getPrefixCls } = useConfig(); + const prefixCls = getPrefixCls('table'); + const typographyCls = getPrefixCls('typography'); + const classes = clsx( + prefixCls, + `${prefixCls}--bordered`, + `${prefixCls}--cell-bordered`, + `${prefixCls}--round`, + `${prefixCls}--header-fixed`, + typographyCls, + ); + + return ( +
+
+
{props.children}
+
+ + ); +}; + +export const THead: FC = ({ children }) => { + const { getPrefixCls } = useConfig(); + const prefixCls = getPrefixCls('table'); + const classes = `${prefixCls}-thead`; + return {children}; +}; + +export const TBody: FC = ({ children }) => { + const { getPrefixCls } = useConfig(); + const prefixCls = getPrefixCls('table'); + const classes = `${prefixCls}-body`; + return {children}; +}; + +export const Th: FC = ({ children }) => { + const { getPrefixCls } = useConfig(); + const prefixCls = getPrefixCls('table'); + const classes = `${prefixCls}-th`; + return {children}; +}; + +const getTableComponents = (): Components => { + return { + table: Table, + thead: THead, + tbody: TBody, + th: Th, + }; +}; + +export const components: Components = { + ...getHeadingComponents(), + ...getTableComponents(), + pre: (props) => { + const children = props.children as unknown as ReactElement<{ + className: string; + children: string; + }>; + + const source = children?.props?.children?.trim() || ''; + const language = children?.props?.className?.replace('language-', '') || ''; + + return ( + + ); + }, + code: ({ children }) => {children}, + API: Api, + Code: Code, + Toc: Toc, +}; diff --git a/site/packages/rd-theme/src/components/layout/page.tsx b/site/packages/rd-theme/src/components/layout/page.tsx new file mode 100644 index 000000000..4f5cd6436 --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/page.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import { BaseComponentProps } from '@casts/common'; + +import { getPrefixCls } from '../../common'; +import { Features } from '../doc-features/features'; +import { Hero } from '../doc-features/hero'; +import { Routes } from './routes'; + +import '../styles/page.scss'; + +export const SitePage: FC = () => { + return ( +
+ + + +
+ ); +}; diff --git a/site/packages/rd-theme/src/components/layout/router.tsx b/site/packages/rd-theme/src/components/layout/router.tsx new file mode 100644 index 000000000..80a3ab959 --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/router.tsx @@ -0,0 +1,39 @@ +import { Suspense, useCallback } from 'react'; +import { CircularProgress } from '@casts/progress'; +import { useRd } from '@casts/rd-vite/client'; + +import { ComponentDoc } from './component-doc'; +import { SitePage } from './page'; + +export const Router = () => { + const { matches } = useRd(); + + const currentRoute = matches?.[matches.length - 1].route; + + const renderContent = useCallback(() => { + if (currentRoute?.meta?.layout === 'page') { + return ; + } + + return ; + }, [currentRoute]); + + return ( + + + + } + > + {renderContent()} + + ); +}; diff --git a/site/packages/rd-theme/src/components/layout/routes.tsx b/site/packages/rd-theme/src/components/layout/routes.tsx new file mode 100644 index 000000000..8de63d44c --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/routes.tsx @@ -0,0 +1,33 @@ +import { FC, RefObject, useEffect } from 'react'; +import { useRoutes } from 'react-router-dom'; +import { rdProvider } from 'virtual:rd-provider'; + +export type RoutesProps = { + containerRef?: RefObject; +}; + +export const Routes: FC = (props) => { + const { containerRef } = props; + const { routes } = rdProvider; + const element = useRoutes(routes); + + /** scroll to url hash */ + useEffect(() => { + if (!containerRef?.current) { + return; + } + + const { hash } = window.location; + if (!hash) { + return; + } + + const target = containerRef.current.querySelector(decodeURIComponent(hash)); + if (!target) { + return; + } + target.scrollIntoView(true); + }, [element, containerRef]); + + return element; +}; diff --git a/site/packages/rd-theme/src/components/layout/sidebar.tsx b/site/packages/rd-theme/src/components/layout/sidebar.tsx new file mode 100644 index 000000000..2a529fbab --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/sidebar.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; +import { Menu } from '@casts/menu'; +import { useRd } from '@casts/rd-vite/client/hooks/use-rd'; +import { useLocation } from 'react-router-dom'; + +import { getPrefixCls } from '../../common'; +import { SidebarProps } from '../../types'; + +export const Sidebar: FC = (props) => { + const { operations } = props; + const { menu } = useRd(); + const location = useLocation(); + + return ( + + ); +}; diff --git a/site/packages/rd-theme/src/components/layout/toc.tsx b/site/packages/rd-theme/src/components/layout/toc.tsx new file mode 100644 index 000000000..05b6d520c --- /dev/null +++ b/site/packages/rd-theme/src/components/layout/toc.tsx @@ -0,0 +1,34 @@ +import { FC, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { Anchor, AnchorProps } from '@casts/anchor'; +import { BaseComponentProps, isCanUseDocument } from '@casts/common'; +import clsx from 'clsx'; + +import { getPrefixCls } from '../../common'; + +export type TocProps = BaseComponentProps & { + data?: AnchorProps['items']; +}; + +export const Toc: FC = (props) => { + const { className, data } = props; + + const prefixCls = getPrefixCls('toc'); + + const classes = clsx(prefixCls, className); + + const containerRef = useRef( + isCanUseDocument() ? document.querySelector('.rd-main-content') : null, + ); + + if (!containerRef.current) { + return null; + } + + const children = ( +
+ +
+ ); + return createPortal(children, containerRef.current); +}; diff --git a/site/packages/rd-theme/src/components/playground/code.scss b/site/packages/rd-theme/src/components/playground/code.scss new file mode 100644 index 000000000..1c05c34f7 --- /dev/null +++ b/site/packages/rd-theme/src/components/playground/code.scss @@ -0,0 +1,76 @@ +// @import '@casts/theme/styles/scss/core'; +@import '@casts/theme/styles/scss/vars/core'; + +@import '@casts/theme/styles/plugins/helper/scrollbar'; + +$playground-prefix-cls: rd-code-block; + +.#{$playground-prefix-cls} { + position: relative; + z-index: $elevation-z-index-low; + background-color: $color-surface-container-default; + border: $border-width-xsmall solid $color-border-component-default; + border-radius: $radius-medium; + + &-loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + } + + &-previewer { + padding: $space-3-x $space-1025-x; + overflow: auto; + border-radius: 0; + + @include scrollbar(8px, 2px, false); + } + + &-toolbar { + display: flex; + justify-content: space-between; + + height: $size-large; + padding-right: $space-1-x; + + padding-left: $space-1-x; + line-height: $size-large; + + border-top: $border-width-xsmall dashed $color-border-component-default; + + &-left, + &-right { + display: inline-flex; + align-items: center; + } + } + + &-source { + border-top: $border-width-xsmall dashed $color-border-component-default; + + .#{$prefix-cls}-code { + &, + pre { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + .#{$prefix-cls}-tabs { + &-bar { + padding-right: $space-05-x; + padding-left: $space-05-x; + + &::after { + display: none; + } + } + + &-content { + padding: 0; + border-top: $border-width-xsmall dashed $color-border-component-default; + } + } + } +} diff --git a/site/packages/rd-theme/src/components/playground/code.tsx b/site/packages/rd-theme/src/components/playground/code.tsx new file mode 100644 index 000000000..28ecca78b --- /dev/null +++ b/site/packages/rd-theme/src/components/playground/code.tsx @@ -0,0 +1,119 @@ +import { + createElement, + FC, + isValidElement, + lazy, + ReactNode, + Suspense, + useEffect, + useState, +} from 'react'; +import { Button } from '@casts/button'; +import { Code as BaseCode } from '@casts/code'; +import { BaseComponentProps, isEmpty, zipObject } from '@casts/common'; +import { CodeSSlashLine } from '@casts/icons'; +import { CircularProgress } from '@casts/progress'; +import { TabPane, Tabs } from '@casts/tabs'; + +import { getPrefixCls } from '../../common'; + +import './code.scss'; + +export type CodeProps = BaseComponentProps & { + src: string; + importSourcesString?: string; + codeSources?: Record; + children: ReactNode; +}; + +export const Code: FC = (props) => { + const { codeSources } = props; + + const prefixCls = getPrefixCls('code-block'); + + /* --------------------------------- get imported source codes ---------------------------------------- */ + const [codes, setCodes] = useState>({}); + + const initCodes = async ( + codeSources: Record Promise<{ default: string }>>, + ) => { + const contents = await Promise.all( + Object.values(codeSources).map( + async (codeDynamicImport) => (await codeDynamicImport()).default, + ), + ); + + const codes = zipObject(Object.keys(codeSources), contents); + setCodes(codes); + }; + + useEffect(() => { + if (!codeSources) { + return; + } + initCodes(codeSources); + }, [codeSources]); + + const initComponent = (component: ReactNode) => { + if (!component || isValidElement(component)) { + return component; + } + + if (typeof component === 'function') { + if (String(component).includes('import(')) { + return createElement(lazy(component)); + } + + return createElement(component); + } + + return component; + }; + + const [component] = useState(() => initComponent(props.children)); + + const [codeVisible, setCodeVisible] = useState(false); + + return ( +
+
+ + +
+ } + > + {component} + +
+
+
+ {/** left */} + {/** */} +
+
+
+
+ {codeVisible && !isEmpty(codes) && ( +
+ + {Object.entries(codes || {}).map(([name, code]) => ( + +
+ +
+
+ ))} +
+
+ )} + + ); +}; diff --git a/site/packages/rd-theme/src/components/playground/externail-link.tsx b/site/packages/rd-theme/src/components/playground/externail-link.tsx new file mode 100644 index 000000000..8e2af8518 --- /dev/null +++ b/site/packages/rd-theme/src/components/playground/externail-link.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react'; +import { Button } from '@casts/button'; +import { BaseComponentProps } from '@casts/common'; +import { ExternalLinkLine } from '@casts/icons'; + +export type ExternalLinkProps = BaseComponentProps; + +export const ExternalLink: FC = () => { + return ( + + ); +}; diff --git a/site/packages/rd-theme/src/components/playground/use-code-sandbox.ts b/site/packages/rd-theme/src/components/playground/use-code-sandbox.ts new file mode 100644 index 000000000..0962dcd88 --- /dev/null +++ b/site/packages/rd-theme/src/components/playground/use-code-sandbox.ts @@ -0,0 +1,7 @@ +export const useCodeSandbox = () => { + const create = () => { + console.log('debug create useCodeSandbox'); + }; + + return { create }; +}; diff --git a/site/packages/rd-theme/src/components/store/context.tsx b/site/packages/rd-theme/src/components/store/context.tsx new file mode 100644 index 000000000..161ac1a65 --- /dev/null +++ b/site/packages/rd-theme/src/components/store/context.tsx @@ -0,0 +1,30 @@ +import { createContext, useContext } from 'react'; +import { noop, useSetState } from '@casts/common'; + +export type AppContextValue = { + themeMode: 'default' | 'dark' | 'custom'; + + isAsideShouldFloat: boolean; + asideVisible: boolean; + toggleAsideVisible: () => void; +}; + +export type AppContextValueWithUpdater = AppContextValue & { + setAppContext: ReturnType>[1]; +}; + +export const defaultAppContextValue: AppContextValueWithUpdater = { + themeMode: 'default', + setAppContext: noop, + + /** aside hook returns */ + isAsideShouldFloat: false, + asideVisible: false, + toggleAsideVisible: noop, +}; + +export const AppContext = createContext( + defaultAppContextValue, +); + +export const useAppContext = () => useContext(AppContext); diff --git a/site/packages/rd-theme/src/components/store/provider.tsx b/site/packages/rd-theme/src/components/store/provider.tsx new file mode 100644 index 000000000..a5d87ebed --- /dev/null +++ b/site/packages/rd-theme/src/components/store/provider.tsx @@ -0,0 +1,37 @@ +import { FC, ReactNode, useEffect } from 'react'; +import { useSetState } from '@casts/common'; + +import { useAside } from '../hooks'; +import { AppContext, AppContextValue, defaultAppContextValue } from './context'; + +export const AppProvider: FC<{ children: ReactNode }> = (props) => { + const { children } = props; + + const asideHook = useAside(); + + const [appContext, setAppContext] = useSetState( + defaultAppContextValue, + ); + + useEffect(() => { + // only set theme mode when it is root appContext provider + if (!appContext.themeMode) { + return; + } + + document.documentElement.setAttribute('theme-mode', appContext.themeMode); + document.documentElement.removeAttribute('theme-palette'); + }, [appContext.themeMode]); + + return ( + + {children} + + ); +}; diff --git a/site/packages/rd-theme/src/components/styles/_vars.scss b/site/packages/rd-theme/src/components/styles/_vars.scss new file mode 100644 index 000000000..bb9d80d71 --- /dev/null +++ b/site/packages/rd-theme/src/components/styles/_vars.scss @@ -0,0 +1,6 @@ +@import '@casts/theme/styles/scss/vars/core'; + +$rd-prefix-cls: rd; +$content-max-width: $breakpoint-xxlarge; +$header-height: 64px; +$doc-banner-height--sticky: 64px; diff --git a/site/packages/rd-theme/src/components/styles/app.scss b/site/packages/rd-theme/src/components/styles/app.scss new file mode 100644 index 000000000..532212562 --- /dev/null +++ b/site/packages/rd-theme/src/components/styles/app.scss @@ -0,0 +1,300 @@ +@import '@casts/theme/styles/scss/vars/core'; +@import '@casts/theme/styles/plugins/global'; +@import '@casts/theme/styles/plugins/motion/fade'; +@import '@casts/theme/styles/plugins/motion/slide'; +@import '@casts/theme/styles/plugins/helper/scrollbar'; +@import './vars'; + +::view-transition-old(root), +::view-transition-new(root) { + mix-blend-mode: normal; + animation: none; +} + +html { + height: 100%; +} + +:root { + scroll-padding-top: 160px; +} + +:root[theme-mode='default'] { + body { + background-color: $color-surface-container-default; + } +} + +.demo-full { + width: 100%; +} + +.#{$rd-prefix-cls}-root-layout { + min-height: 100vh; + min-height: 100dvh; +} + +.#{$rd-prefix-cls}-sub-layout { + flex: 1; +} + +.#{$rd-prefix-cls}-header { + position: sticky; + top: 0; + z-index: $elevation-z-index-sticky; + background-color: inherit; + box-shadow: 0px 0px 12px -2px hsla(0, 0%, 0%, 0.08); + will-change: change; + + .#{$rd-prefix-cls}-logo { + display: inline-flex; + } + + .#{$rd-prefix-cls}-aside-collapse-button { + margin-right: $space-075-x; + font-size: $font-size-large; + } +} + +.#{$rd-prefix-cls}-navbar { + position: relative; + z-index: $elevation-z-index-sticky; + border-bottom: $border-width-xsmall solid $color-border-component-default; +} + +.#{$rd-prefix-cls}-aside { + $aside-width: 240px; + position: relative; + z-index: $elevation-z-index-low; + flex-shrink: 0; + + & &-collapse-button { + position: fixed; + top: 30vh; + left: $aside-width; + z-index: $elevation-z-index-sticky; + border-left-color: transparent; + border-top-right-radius: $radius-medium; + border-bottom-right-radius: $radius-medium; + box-shadow: $shadow-level-2; + transition: left $motion-duration-rapid $motion-easing-in-out; + + &.#{$rd-prefix-cls}-button--contained { + background-color: #fff; + } + } + + &-content { + position: sticky; + top: $header-height; + width: $aside-width; + } + + &-overlay { + @media screen and (max-width: calc($breakpoint-medium - 1px)) { + position: fixed; + top: 0; + bottom: 0; + z-index: $elevation-z-index-mask; + width: 100%; + height: 100%; + background-color: hsla($color-palette-hsl-neutral-1000, $opacity-38); + } + } + + .#{$rd-prefix-cls}-menu { + position: relative; + z-index: $elevation-z-index-sticky; + height: 100%; + min-height: calc(100vh - $header-height); + min-height: calc(100dvh - $header-height); + max-height: calc(100vh - $header-height); + max-height: calc(100dvh - $header-height); + padding-bottom: 64px; + + overflow-y: auto; + overflow-y: overlay; + + border-right: $border-width-xsmall solid $color-border-component-default; + + @include scrollbar(8px, 2px, true); + } + + // aside animation + @media screen and (max-width: calc($breakpoint-medium - 1px)) { + width: 0px; + + &.is-hide { + .#{$rd-prefix-cls}-aside-collapse-button { + left: 0px; + } + } + + .#{$rd-prefix-cls}-aside-animate { + &-enter .rd-menu { + display: block; + transform: translate3d(-100%, 0%, 1px); + } + + &-enter-active .rd-menu { + transition: transform $motion-duration-rapid $motion-easing-in-out; + transform: translate3d(0%, 0%, 1px); + } + + &-exit .rd-menu { + transform: translate3d(0%, 0%, 1px); + } + + &-exit-active .rd-menu { + transition: transform $motion-duration-rapid $motion-easing-in-out; + transform: translate3d(-100%, 0%, 1px); + } + + &-exit-done .rd-menu { + display: none; + } + } + } +} + +.#{$rd-prefix-cls}-content-layout { + position: relative; + z-index: 0; + flex: 1; + margin: 0 auto; + + .#{$rd-prefix-cls}-main-content { + display: flex; + max-width: $content-max-width; + min-height: 100vh; + min-height: 100dvh; + margin: 0 auto; + + .markdown { + position: relative; + box-sizing: border-box; + flex: 1; + width: 0; + min-width: 0; + max-width: 100%; + padding: $space-1-x $space-2-x; + overflow: hidden; + font-size: $font-size-small; + + & > .#{$prefix-cls}-code { + margin-top: $space-05-x; + margin-bottom: $space-1-x; + } + + p > .#{$prefix-cls}-typography { + font-size: inherit; + } + + .#{$prefix-cls}-code { + pre { + border: 0px; + } + } + } + + h1, + h2, + h3, + h4, + h5, + h6 { + &.#{$prefix-cls}-typography { + position: relative; + display: flex; + align-items: center; + margin-top: $space-2-x; + margin-bottom: $space-105-x; + } + + .#{$rd-prefix-cls}-heading-link { + position: absolute; + display: inline-flex; + align-items: center; + padding-right: $space-025-x; + opacity: 0; + transition: opacity $motion-duration-rapid $motion-easing-in-out; + transform: translateX(-100%); + + &-icon { + width: 1em; + height: 1em; + fill: currentColor; + transform: scale(0.84); + } + } + + &:hover { + .#{$rd-prefix-cls}-heading-link { + opacity: 1; + } + } + + .#{$rd-prefix-cls}-heading-link:focus-visible { + outline: none; + opacity: 1; + } + } + } +} + +.#{$rd-prefix-cls}-footer { + padding-top: $space-2-x; + padding-bottom: $space-1-x; + border-top: 1px solid $color-border-divider-default; + + .#{$rd-prefix-cls}-copyright { + color: $color-text-secondary; + text-align: center; + + p { + margin: 0; + + & + p { + margin-top: $space-05-x; + } + } + } +} + +.#{$rd-prefix-cls}-toc { + position: sticky; + top: 150px; + height: fit-content; + min-height: 1px; + max-height: calc(100vh - 240px); + padding-top: 10px; + padding-right: $space-2-x; + overflow-y: auto; + overflow-y: overlay; + + @include scrollbar(); + + @media screen and (max-width: $breakpoint-large) { + display: none; + } + + .#{$prefix-cls}-anchor { + width: 168px; + } +} + +.#{$rd-prefix-cls}-root-layout { + .#{$rd-prefix-cls}-float-button { + position: fixed; + right: $space-2-x; + bottom: $space-2-x; + } +} + +.#{$rd-prefix-cls}-theme-switcher { + display: inline-flex; + align-items: center; + font-size: $font-size-medium; + color: $color-text-primary; + transition: color $motion-duration-rapid $motion-easing-in-out; +} diff --git a/site/packages/rd-theme/src/components/styles/doc-banner.scss b/site/packages/rd-theme/src/components/styles/doc-banner.scss new file mode 100644 index 000000000..8038709d6 --- /dev/null +++ b/site/packages/rd-theme/src/components/styles/doc-banner.scss @@ -0,0 +1,88 @@ +@import '@casts/theme/styles/scss/vars/core'; +@import '@casts/theme/styles/plugins/helper/overflow'; + +@import './vars'; + +.#{$rd-prefix-cls}-doc-banner { + box-sizing: border-box; + max-width: $content-max-width; + padding: $space-2-x; + margin: auto; + overflow: hidden; + + &-container { + position: relative; + background-color: $color-surface-container-default; + border-bottom: $border-width-xsmall solid $color-border-component-default; + opacity: 1; + transition: opacity $motion-duration-immediate $motion-easing-in-out; + + &--fixed { + opacity: 0; + } + + &--sticky { + position: sticky; + top: $header-height; + z-index: $elevation-z-index-sticky; + margin-top: -77px; + visibility: hidden; + opacity: 0; + + &.#{$rd-prefix-cls}-doc-banner-container--fixed { + visibility: visible; + opacity: 1; + } + + .#{$rd-prefix-cls}-doc-banner-title { + text-overflow: ellipsis; + @include text-ellipsis(); + } + } + } + + &-breadcrumbs { + width: fit-content; + padding: $space-05-x $space-075-x; + margin-bottom: $space-105-x; + background-color: $color-surface-neutral-default; + border-radius: $radius-medium; + } + + &-subtitle { + margin-top: $space-1-x; + margin-bottom: 0; + color: $color-text-tertiary; + } + + &-divider { + margin-top: 0; + margin-bottom: 0; + } + + .#{$rd-prefix-cls}-doc-banner--sticky { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + } + + &-title-affix { + position: absolute; + bottom: 0px; + width: 100%; + } + + &--sticky { + padding-top: $space-1-x; + padding-bottom: $space-1-x; + + background-color: $color-surface-container-default; + } + + & &-title { + &--sticky { + margin: calc(0 - $space-2-x) !important; + } + } +} diff --git a/site/packages/rd-theme/src/components/styles/page.scss b/site/packages/rd-theme/src/components/styles/page.scss new file mode 100644 index 000000000..3781aeefe --- /dev/null +++ b/site/packages/rd-theme/src/components/styles/page.scss @@ -0,0 +1,141 @@ +@import '@casts/theme/styles/scss/vars/core'; +@import '@casts/theme/styles/plugins/helper/overflow'; + +@import './vars'; + +.#{$rd-prefix-cls}-page { + max-width: $breakpoint-xxlarge; + padding: $space-2-x $space-105-x; + margin: auto; + overflow: hidden; +} + +.rd-banner { + &-container { + position: relative; + display: flex; + padding: $space-2-x 0; + } + + &-bg { + position: absolute; + top: 0; + right: calc(0px - #{$space-105-x}); + bottom: 0; + left: calc(0px - #{$space-105-x}); + overflow: hidden; + + svg { + position: absolute; + // z-index: -1; + bottom: 0; + } + } + + &-content { + display: flex; + flex-direction: column; + text-align: center; + } + + &-hero { + margin: 0; + font-size: $font-size-xxxlarge; + line-height: $line-height-comfortable; + color: transparent; + + background-color: rgb(202, 86, 218); + background-image: linear-gradient( + 135deg, + rgba(202, 86, 218, 1) 0%, + rgba(221, 53, 65, 1) 100% + ); + -webkit-background-clip: text; + } + + &-description { + margin: 0; + font-size: $font-size-xxlarge; + line-height: $line-height-comfortable; + color: $color-text-primary; + } + + &-actions { + margin-top: $space-2-x; + } + + &-image-container { + display: flex; + justify-content: center; + padding: $space-3-x; + } + + &-logo { + width: 100%; + max-width: 320px; + color: $color-shape-neutral-default; + fill: currentColor; + } + + @media (max-width: $breakpoint-medium) { + &-container { + flex-direction: column-reverse; + padding-top: 0px; + } + + &-hero { + font-size: $font-size-xxlarge; + line-height: $line-height-comfortable; + } + + &-description { + margin: 0; + font-size: $font-size-xlarge; + line-height: $line-height-comfortable; + } + + &-image-container { + padding: $space-1-x; + } + + &-logo { + max-width: 160px; + } + } +} + +.rd-features-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: $space-1-x; + margin-top: $space-2-x; + + @media (max-width: $breakpoint-medium) { + grid-template-columns: repeat(1, 1fr); + } +} + +.rd-feature-card { + padding: $space-1-x; + overflow: hidden; + background-color: $color-surface-neutral-default; + border-radius: $radius-medium; + + &-icon { + margin-bottom: $space-05-x; + font-size: $font-size-xlarge; + line-height: $line-height-xlarge; + text-align: center; + } + + &-title { + margin-bottom: $space-05-x; + font-size: $font-size-large; + line-height: $line-height-large; + text-align: center; + } + + &-content { + color: $color-text-secondary; + } +} diff --git a/site/packages/rd-theme/src/hooks/index.ts b/site/packages/rd-theme/src/hooks/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/site/packages/rd-theme/src/hooks/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/site/packages/rd-theme/src/index.ts b/site/packages/rd-theme/src/index.ts new file mode 100644 index 000000000..5dc1cb0b1 --- /dev/null +++ b/site/packages/rd-theme/src/index.ts @@ -0,0 +1,5 @@ +export * from './common'; +export * from './components'; +export * from './hooks'; +export * from './types'; +export * from './utils'; diff --git a/site/packages/rd-theme/src/storage/theme.ts b/site/packages/rd-theme/src/storage/theme.ts new file mode 100644 index 000000000..d871e7007 --- /dev/null +++ b/site/packages/rd-theme/src/storage/theme.ts @@ -0,0 +1,7 @@ +import { BrowserStorage } from '@casts/common'; +import { MainColor, ThemeMode } from '@casts/theme-generator'; + +export const themeStorage = new BrowserStorage<{ + mode: ThemeMode; + mainColors: MainColor[]; +}>('theme'); diff --git a/site/packages/rd-theme/src/theme/index.ts b/site/packages/rd-theme/src/theme/index.ts new file mode 100644 index 000000000..8b3b3bf91 --- /dev/null +++ b/site/packages/rd-theme/src/theme/index.ts @@ -0,0 +1,32 @@ +import { MainColor } from '@casts/theme-generator'; + +export const defaultMainColors: MainColor[] = [ + { + name: 'brand', + color: '#CA56DA', + }, + { + name: 'secondary', + color: '#DD3541', + }, + { + name: 'info', + color: '#2191FB', + }, + { + name: 'success', + color: '#4CB944', + }, + { + name: 'warning', + color: '#EA591F', + }, + { + name: 'danger', + color: '#E42535', + }, + // { + // name: 'neutral', + // color: '#666666', + // }, +]; diff --git a/site/packages/rd-theme/src/types/index.ts b/site/packages/rd-theme/src/types/index.ts new file mode 100644 index 000000000..ad72d31f6 --- /dev/null +++ b/site/packages/rd-theme/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './locale'; +export * from './sidebar'; diff --git a/site/packages/rd-theme/src/types/locale.ts b/site/packages/rd-theme/src/types/locale.ts new file mode 100644 index 000000000..f6214e4a5 --- /dev/null +++ b/site/packages/rd-theme/src/types/locale.ts @@ -0,0 +1,7 @@ +type BasicLocale = { id: string; name: string }; + +export type Locale = + | (BasicLocale & { base: string }) + | (BasicLocale & { suffix: string }); + +export type LocalesConfig = Locale[]; diff --git a/site/packages/rd-theme/src/types/sidebar.ts b/site/packages/rd-theme/src/types/sidebar.ts new file mode 100644 index 000000000..9362bef0e --- /dev/null +++ b/site/packages/rd-theme/src/types/sidebar.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; +import { BaseComponentProps } from '@casts/common'; + +export type SidebarProps = BaseComponentProps & { + operations?: ReactNode; +}; diff --git a/site/packages/rd-theme/src/utils/index.ts b/site/packages/rd-theme/src/utils/index.ts new file mode 100644 index 000000000..c0912ba85 --- /dev/null +++ b/site/packages/rd-theme/src/utils/index.ts @@ -0,0 +1 @@ +export * from './is-link-click'; diff --git a/site/packages/rd-theme/src/utils/is-link-click.ts b/site/packages/rd-theme/src/utils/is-link-click.ts new file mode 100644 index 000000000..e1fef7e3f --- /dev/null +++ b/site/packages/rd-theme/src/utils/is-link-click.ts @@ -0,0 +1,19 @@ +import { MouseEvent } from 'react'; + +export const isLinkClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + + const isLink = (el: HTMLElement | null): boolean => { + if (!el) { + return false; + } + + if (el.tagName === 'A') { + return true; + } + + return isLink(el.parentNode as HTMLElement | null); + }; + + return isLink(target); +}; diff --git a/site/packages/rd-vite/README.md b/site/packages/rd-vite/README.md new file mode 100644 index 000000000..357352fb8 --- /dev/null +++ b/site/packages/rd-vite/README.md @@ -0,0 +1 @@ +# @casts/rd-vite diff --git a/site/packages/rd-vite/docs/api-parser.md b/site/packages/rd-vite/docs/api-parser.md new file mode 100644 index 000000000..c1fa4aa9e --- /dev/null +++ b/site/packages/rd-vite/docs/api-parser.md @@ -0,0 +1,14 @@ +# Api Parser + +## Flowchart + +```mermaid +graph TB + start(parser start) --> collect[collect apis from files] + + collect --> write[write apis to api.json] + + write --> render[render the apis] + + update(update) -- "update api.json" --> collect +``` diff --git a/site/packages/rd-vite/package.json b/site/packages/rd-vite/package.json new file mode 100644 index 000000000..bd99d666a --- /dev/null +++ b/site/packages/rd-vite/package.json @@ -0,0 +1,12 @@ +{ + "name": "@casts/rd-vite", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "moecasts", + "license": "ISC" +} \ No newline at end of file diff --git a/site/packages/rd-vite/src/client/components/context.tsx b/site/packages/rd-vite/src/client/components/context.tsx new file mode 100644 index 000000000..3f9537c2f --- /dev/null +++ b/site/packages/rd-vite/src/client/components/context.tsx @@ -0,0 +1,54 @@ +import { createContext, FC, useMemo } from 'react'; +import { BaseComponentProps } from '@casts/common'; +import { MenuObject } from '@casts/menu'; +import { Navigator } from '@casts/rd-vite/types/navigator'; +import { matchRoutes, RouteObject, useLocation } from 'react-router-dom'; +import { rdProvider } from 'virtual:rd-provider'; + +import { LocaleCode, localeCodes } from '../../common/locale'; +import { Source } from '../../types'; +import { useCurrentLocale } from '../hooks'; +import { getMenuDataFromRoute } from '../utils/route'; + +export const RdContext = createContext<{ + /** + * Site name + */ + name?: string; + menu?: MenuObject[]; + sources?: Source[]; + nav?: Navigator[]; + matches?: Array<{ pathname: string; route: RouteObject }> | null; + localeCode?: LocaleCode; +}>({}); + +export type RdProviderProps = BaseComponentProps; + +export const RdProvider: FC = (props) => { + const { children } = props; + + const { routes, sources, navigators, ...rest } = rdProvider; + + const location = useLocation(); + const [localeCode] = useCurrentLocale(localeCodes); + + const matches = useMemo(() => matchRoutes(routes, location), [location]); + + const menu = useMemo( + () => getMenuDataFromRoute({ routes, matches, localeCode }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [localeCode, matches], + ); + + const nav = useMemo(() => { + return navigators[localeCode] || []; + }, [localeCode, navigators]); + + return ( + + {children} + + ); +}; diff --git a/site/packages/rd-vite/src/client/components/index.ts b/site/packages/rd-vite/src/client/components/index.ts new file mode 100644 index 000000000..dfe284e1e --- /dev/null +++ b/site/packages/rd-vite/src/client/components/index.ts @@ -0,0 +1,2 @@ +export * from './context'; +export * from './non-leaf-route-outlet'; diff --git a/site/packages/rd-vite/src/client/components/non-leaf-route-outlet.tsx b/site/packages/rd-vite/src/client/components/non-leaf-route-outlet.tsx new file mode 100644 index 000000000..d8cd7bc19 --- /dev/null +++ b/site/packages/rd-vite/src/client/components/non-leaf-route-outlet.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect } from 'react'; +import { head, last } from '@casts/common'; +import { + matchRoutes, + Outlet, + useLocation, + useNavigate, +} from 'react-router-dom'; +import { rdProvider } from 'virtual:rd-provider'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export const NonLeafRouteOutlet: FC<{}> = () => { + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + const matches = matchRoutes(rdProvider.routes, location); + const currentRoute = last(matches)?.route; + + if (currentRoute?.children) { + const to = head(currentRoute.children)?.meta?.absPath; + to && navigate(to); + } + }, [location, navigate]); + + return ; +}; diff --git a/site/packages/rd-vite/src/client/hooks/index.ts b/site/packages/rd-vite/src/client/hooks/index.ts new file mode 100644 index 000000000..069f2fdd5 --- /dev/null +++ b/site/packages/rd-vite/src/client/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-current-locale'; +export * from './use-rd'; diff --git a/site/packages/rd-vite/src/client/hooks/use-current-locale.ts b/site/packages/rd-vite/src/client/hooks/use-current-locale.ts new file mode 100644 index 000000000..73e31573f --- /dev/null +++ b/site/packages/rd-vite/src/client/hooks/use-current-locale.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { fallbackLocaleCode, LocaleCodes } from '../../common/locale'; + +export const useCurrentLocale = (localeCodes: LocaleCodes) => { + const localeRE = useMemo( + () => new RegExp(`^/(${localeCodes.join('|')})`), + [localeCodes], + ); + + const location = useLocation(); + + const getCurrentLocale = useCallback(() => { + const locale = localeRE.exec(location.pathname)?.[1]; + return locale || fallbackLocaleCode; + }, [localeRE, location.pathname]); + + const [locale, setLocale] = useState(() => getCurrentLocale()); + + useEffect(() => { + const locale = getCurrentLocale(); + setLocale(locale || fallbackLocaleCode); + }, [getCurrentLocale]); + + return [locale, locale === fallbackLocaleCode] as const; +}; diff --git a/site/packages/rd-vite/src/client/hooks/use-locale-location.ts b/site/packages/rd-vite/src/client/hooks/use-locale-location.ts new file mode 100644 index 000000000..5ec44de27 --- /dev/null +++ b/site/packages/rd-vite/src/client/hooks/use-locale-location.ts @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; +import { useMemo } from 'react'; +import { + fallbackLocaleCode, + LocaleCode, + LocaleCodes, +} from '@casts/rd-vite/common'; +import { useLocation } from 'react-router-dom'; + +import { useCurrentLocale } from './use-current-locale'; +import { useRd } from './use-rd'; + +export const useLocaleLocation = (localeCodes: LocaleCodes) => { + const [locale] = useCurrentLocale(localeCodes); + const location = useLocation(); + + const firstDiffLocale = useMemo( + () => localeCodes.find((code) => code !== locale) || fallbackLocaleCode, + [locale, localeCodes], + ); + + const { matches } = useRd(); + + const getRouteBase = useCallback((): string => { + const baseRoute = matches?.[0]; + return baseRoute?.pathname || ''; + }, [matches]); + + const getLocaleLocation = useCallback( + (targetLocaleCode: LocaleCode) => { + // const base = location.pathname; + const base = getRouteBase(); + + const baseWithoutLocale = base.replace(`/${locale}`, ''); + + const pathnameWithoutLocale = + location.pathname.replace( + new RegExp(`^${base}(/|$)`), + `${baseWithoutLocale}$1`, + ) || '/'; + + if (targetLocaleCode !== fallbackLocaleCode) { + const routePrefix = `${baseWithoutLocale}/${targetLocaleCode}`.replace( + /\/\//, + '/', + ); + const pathnameWithoutBase = location.pathname.replace( + // to avoid stripped the first / + base.replace(/^\/$/, '//'), + '', + ); + + return `${routePrefix}${pathnameWithoutBase}`.replace(/\/\//g, '/'); + } + + return pathnameWithoutLocale; + }, + [getRouteBase, locale, location.pathname], + ); + + return { firstDiffLocale, getLocaleLocation }; +}; diff --git a/site/packages/rd-vite/src/client/hooks/use-rd.ts b/site/packages/rd-vite/src/client/hooks/use-rd.ts new file mode 100644 index 000000000..5eec23cf0 --- /dev/null +++ b/site/packages/rd-vite/src/client/hooks/use-rd.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { RdContext } from '../components/context'; + +export const useRd = () => useContext(RdContext); diff --git a/site/packages/rd-vite/src/client/index.ts b/site/packages/rd-vite/src/client/index.ts new file mode 100644 index 000000000..fd70c4250 --- /dev/null +++ b/site/packages/rd-vite/src/client/index.ts @@ -0,0 +1,2 @@ +export * from './hooks'; +export * from './utils'; diff --git a/site/packages/rd-vite/src/client/utils/index.ts b/site/packages/rd-vite/src/client/utils/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/site/packages/rd-vite/src/client/utils/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/site/packages/rd-vite/src/client/utils/route.ts b/site/packages/rd-vite/src/client/utils/route.ts new file mode 100644 index 000000000..69b9c99b7 --- /dev/null +++ b/site/packages/rd-vite/src/client/utils/route.ts @@ -0,0 +1,94 @@ +import { ReactNode } from 'react'; +import { + findKey, + findNodeByPathDFS, + groupBy, + map, + reduce, + sortBy, +} from '@casts/common'; +import { MenuObject } from '@casts/menu'; +import { matchRoutes, RouteObject } from 'react-router-dom'; + +import { LocaleCodes } from '../../common'; + +/** convert route to menu item */ +const routeToMenuObject = (route: RouteObject): MenuObject => ({ + key: route.meta?.title as string, + label: route.meta?.title as ReactNode, + href: route.meta?.absPath, + value: route.meta?.absPath, +}); + +export const getMenuDataFromRoute = (payload: { + routes: RouteObject[]; + matches: ReturnType; + localeCode?: LocaleCodes[0]; +}) => { + const { routes, matches } = payload; + + if (!matches) { + return []; + } + + const currentRouteMetaNavPath = ( + matches[matches.length - 1]?.route as RouteObject + )?.meta?.nav?.path; + + const parentPath = `${currentRouteMetaNavPath ?? ''}`; + + const parentMatches = matchRoutes(routes, parentPath); + + const currentLevelRoute = findNodeByPathDFS({ + tree: routes, + path: map(parentMatches, 'route'), + prop: 'path', + }); + + const makeMenuDataFromRoutes = ({ routes }: { routes: RouteObject[] }) => { + const groups = groupBy(routes, 'meta.group.title'); + + const data: MenuObject[] = reduce( + groups, + (acc, item, title) => { + if (title === 'undefined') { + return [ + ...acc, + ...reduce( + item, + (acc, child) => { + if (child.children) { + return [ + ...acc, + ...makeMenuDataFromRoutes({ routes: child.children }), + ]; + } + + return [...acc, routeToMenuObject(child)]; + }, + + [] as MenuObject[], + ), + ]; + } + + const group: MenuObject & { order?: string } = { + type: 'group', + key: title, + label: title, + children: map(item, routeToMenuObject), + order: findKey(item, 'meta.group.order'), + }; + + return [...acc, group]; + }, + [] as MenuObject[], + ); + + return sortBy(data, ['order', 'title']); + }; + + return makeMenuDataFromRoutes({ + routes: currentLevelRoute?.children, + }); +}; diff --git a/site/packages/rd-vite/src/common/index.ts b/site/packages/rd-vite/src/common/index.ts new file mode 100644 index 000000000..5501675d5 --- /dev/null +++ b/site/packages/rd-vite/src/common/index.ts @@ -0,0 +1 @@ +export * from './locale'; diff --git a/site/packages/rd-vite/src/common/locale.ts b/site/packages/rd-vite/src/common/locale.ts new file mode 100644 index 000000000..8d3762802 --- /dev/null +++ b/site/packages/rd-vite/src/common/locale.ts @@ -0,0 +1,14 @@ +export type Locales = [string, string][]; + +export type LocaleCode = string; + +export type LocaleCodes = LocaleCode[]; + +export const locales: Locales = [ + ['en-US', 'English'], + ['zh-CN', '中文'], +]; + +export const localeCodes: LocaleCodes = locales.map((i) => i[0]); + +export const fallbackLocaleCode = localeCodes[0]; diff --git a/site/packages/rd-vite/src/index.ts b/site/packages/rd-vite/src/index.ts new file mode 100644 index 000000000..2986386ce --- /dev/null +++ b/site/packages/rd-vite/src/index.ts @@ -0,0 +1,275 @@ +import { createFormatAwareProcessors } from '@mdx-js/mdx/lib/util/create-format-aware-processors.js'; +import { Options } from '@mdx-js/rollup'; +import { transformSync } from 'esbuild'; +import { readFileSync } from 'fs'; +import { find, isEmpty } from 'lodash-es'; +import path from 'path'; +import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import rehypeSlug from 'rehype-slug'; +import remarkFrontmatter from 'remark-frontmatter'; +import remarkGFM from 'remark-gfm'; +import remarkMdxImages from 'remark-mdx-images'; +import { SourceMapGenerator } from 'source-map'; +import { toVFile } from 'to-vfile'; +import type { Alias, PluginOption } from 'vite'; + +import { Locales, locales } from './common'; +import { detectMarkdowns, remarkReactApi } from './node'; +import { rehypeToc, remarkCodeBlock, remarkCodeBlockReplacer } from './node'; +import { getRuntimeImports } from './node/provider'; +import { generateRoutes } from './node/provider/sitemap'; +import { ResolveFunction, ResolverRef } from './types'; +import { customizeTOC } from './utils'; + +const getMdxOptions = ({ + resolverRef, + root, +}: { + resolverRef: ResolverRef; + root?: string; +}): Options => ({ + providerImportSource: '@mdx-js/react', + mdxExtensions: ['.mdx', '.md'], + format: 'mdx', + remarkPlugins: [ + remarkGFM, + remarkMdxImages, + remarkCodeBlockReplacer, + [remarkCodeBlock, { resolverRef, root }], + [remarkReactApi, { resolve: resolverRef.current }], + ], + rehypePlugins: [ + rehypeSlug, + [ + rehypeToc, + { + cssClasses: { + toc: 'rd-toc', + }, + headings: ['h2', 'h3', 'h4'], + customizeTOC: customizeTOC, + }, + ], + [ + rehypeAutolinkHeadings, + { + properties: { + className: 'rd-heading-link', + }, + content: { + type: 'element', + tagName: 'svg', + properties: { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 24 24', + className: 'rd-heading-link-icon', + }, + children: [ + { + type: 'element', + tagName: 'path', + properties: { + d: 'M13.0605 8.11073L14.4747 9.52494C17.2084 12.2586 17.2084 16.6908 14.4747 19.4244L14.1211 19.778C11.3875 22.5117 6.95531 22.5117 4.22164 19.778C1.48797 17.0443 1.48797 12.6122 4.22164 9.87849L5.63585 11.2927C3.68323 13.2453 3.68323 16.4112 5.63585 18.3638C7.58847 20.3164 10.7543 20.3164 12.7069 18.3638L13.0605 18.0102C15.0131 16.0576 15.0131 12.8918 13.0605 10.9392L11.6463 9.52494L13.0605 8.11073ZM19.778 14.1211L18.3638 12.7069C20.3164 10.7543 20.3164 7.58847 18.3638 5.63585C16.4112 3.68323 13.2453 3.68323 11.2927 5.63585L10.9392 5.98941C8.98653 7.94203 8.98653 11.1079 10.9392 13.0605L12.3534 14.4747L10.9392 15.8889L9.52494 14.4747C6.79127 11.741 6.79127 7.30886 9.52494 4.57519L9.87849 4.22164C12.6122 1.48797 17.0443 1.48797 19.778 4.22164C22.5117 6.95531 22.5117 11.3875 19.778 14.1211Z', + }, + }, + ], + }, + }, + ], + ], + recmaPlugins: [remarkFrontmatter], +}); + +export type RdConfig = { + root?: string; + dotRoot?: string; + locales?: Locales; + + /** + * Site name + */ + name?: string; +}; + +export const rd = (config: RdConfig = {}) => { + return [createPlugin(config)]; +}; + +export const getDefaultRdConfig = (): RdConfig => ({ + locales, +}); + +const createPlugin = (userConfig: RdConfig = {}) => { + const config: RdConfig = Object.assign({}, getDefaultRdConfig(), userConfig); + + const virtualModuleId = 'virtual:rd-provider'; + const resolvedVirtualModuleId = `\0${virtualModuleId}`; + + let docRoot = config?.root || '../'; + let root: string; + + let processors: ReturnType; + + let resolve: ResolveFunction; + + let alias: Alias[]; + + let routes: any[] = []; + + const plugin: PluginOption = { + name: 'rd-vite', + enforce: 'pre', + + config: () => ({ + resolve: { + modules: ['@mdx-js/react'], + alias: { + '@root': docRoot, + '@casts/rd-vite': path.resolve(__dirname), + }, + }, + }), + + resolveId: (id) => { + if (id === virtualModuleId) { + return resolvedVirtualModuleId; + } + + return undefined; + }, + + load(id) { + if (id === resolvedVirtualModuleId) { + const markdownSources = detectMarkdowns(docRoot); + const providerContent = readFileSync( + path.join(__dirname, './node/provider/virtual.ts'), + ); + + routes = generateRoutes({ sources: markdownSources }); + + const content = ` + ${providerContent.toString()}; + export const rdProvider = initRdProvider({ + name: ${JSON.stringify(config.name)}, + sources: ${JSON.stringify(markdownSources)}, + runtimeImports: ${getRuntimeImports(markdownSources)}, + }); + `; + + return transformSync(content, { loader: 'tsx' }); + } + + return undefined; + }, + + // config: (config, { command }) => { + // console.log('这里是config钩子', { config, command }); + // }, + + configResolved(resolvedConfig) { + // console.log('这里是configResolved钩子', { resolvedConfig }); + if (!docRoot) { + docRoot = resolvedConfig.root; + } + if (!root) { + root = resolvedConfig.root; + } + alias = resolvedConfig.resolve.alias; + }, + async transform(value, path) { + if (!resolve) { + resolve = (source, importer, options) => { + const mergedOptions: typeof options = Object.assign( + { + skipSelf: true, + }, + options, + ); + + const currentAlias = find(alias, { find: source }); + + if (currentAlias?.replacement) { + const realSource = source.replace(source, currentAlias.replacement); + return this.resolve(realSource, importer, mergedOptions); + } + + return this.resolve(source, importer, mergedOptions); + }; + } + + if (!processors) { + processors = createFormatAwareProcessors({ + SourceMapGenerator, + ...getMdxOptions({ + resolverRef: { current: resolve }, + root, + }), + }); + } + + const { extnames, process } = processors; + const file = toVFile({ value, path }); + + if ( + file.extname && + // filter(file.path) && + extnames.includes(file.extname) + ) { + const compiled = await process(file); + const code = String(compiled.value); + const result = { code, map: compiled.map }; + return result; + } + + return; + // // @ts-ignore mdxPlugin has transform function + // const code = await mdxPlugin.transform?.(value, path); + // + // if (path.includes('tooltip') && code) { + // return { + // ...code, + // // code: "import('/../docs/help.md')" + code.code, + // }; + // } + // return code; + }, + + // handleHotUpdate(ctx) { + // const { server } = ctx; + // console.log('debug handleHotUpdate', server); + // server.ws.send({ + // type: 'full-reload', + // path: '*', + // }); + // }, + + // configureServer(server) { + // console.log('这里是configureServer钩子', { server }); + // }, + + transformIndexHtml(html) { + if (!config.name) { + return html; + } + + return html.replace( + /(.*?)<\/title>/, + `<title>${config.name}`, + ); + }, + + generateBundle() { + if (isEmpty(routes)) { + return; + } + + this.emitFile({ + fileName: 'routes-manifest.json', + type: 'asset', + source: JSON.stringify(routes, undefined, 2), + }); + }, + }; + + return plugin; +}; diff --git a/site/packages/rd-vite/src/node/index.ts b/site/packages/rd-vite/src/node/index.ts new file mode 100644 index 000000000..d5d3e910f --- /dev/null +++ b/site/packages/rd-vite/src/node/index.ts @@ -0,0 +1,3 @@ +export * from './provider'; +export * from './rehypes'; +export * from './remarks'; diff --git a/site/packages/rd-vite/src/node/provider/import.ts b/site/packages/rd-vite/src/node/provider/import.ts new file mode 100644 index 000000000..c34ee0199 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/import.ts @@ -0,0 +1,20 @@ +import { Source } from '../../types'; +import { ensureSlashStartPath } from '../../utils'; + +export const getRuntimeImports = (sources: Source[]) => { + return ` + { + ${sources + .map((source: Source) => { + const id = ensureSlashStartPath(source.path); + return ` + '${id}': { + import: import.meta.glob('${id}')['${id}'], + // raw: import.meta.glob('${id}', { as: 'raw' })['${id}'], + } + `; + }) + .join(',')} + } + `; +}; diff --git a/site/packages/rd-vite/src/node/provider/index.ts b/site/packages/rd-vite/src/node/provider/index.ts new file mode 100644 index 000000000..c12a774dd --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/index.ts @@ -0,0 +1,4 @@ +export * from './import'; +export * from './markdown'; +// export * from './nav'; +// export * from './route'; diff --git a/site/packages/rd-vite/src/node/provider/markdown.ts b/site/packages/rd-vite/src/node/provider/markdown.ts new file mode 100644 index 000000000..c52dedf49 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/markdown.ts @@ -0,0 +1,68 @@ +import rehypeToc from '@jsdevtools/rehype-toc'; +import { compileSync } from '@mdx-js/mdx'; +import fg from 'fast-glob'; +import { isEmpty, reduce } from 'lodash-es'; +import path from 'path'; +import rehypeSlug from 'rehype-slug'; +import { readSync } from 'to-vfile'; +import { matter } from 'vfile-matter'; +import { normalizePath } from 'vite'; + +import { ResolveFunction, Source, SourceData, SourceMeta } from '../../types'; +import { remarkCodeBlockReplacer } from '../remarks'; +import { remarkCodeBlockStandalone } from '../remarks/code-block'; + +export type DetectMarkdownsOptions = { + withMeta?: boolean; + resolve?: ResolveFunction; +}; + +const defaultDetectMarkdownsOptions: Partial = { + withMeta: true, +}; + +export const detectMarkdowns = ( + root: string, + options: DetectMarkdownsOptions = {}, +) => { + const { withMeta } = Object.assign(defaultDetectMarkdownsOptions, options); + + const paths = fg + .sync(['**/docs/**/[^_]*.@(md|mdx)', '**/src/**/[^_]*.@(md|mdx)'], { + cwd: root, + ignore: ['**/node_modules/**'], + }) + .map((p) => { + const item: Source = { + id: normalizePath(p), + path: normalizePath(path.posix.join(root, p)), + data: {}, + }; + if (withMeta) { + const file = readSync(item.path); + item.meta = matter(file).data.matter as SourceMeta; + + const compiledFile = compileSync(file, { + remarkPlugins: [remarkCodeBlockReplacer, remarkCodeBlockStandalone], + rehypePlugins: [rehypeSlug, rehypeToc], + format: 'mdx', + }); + + if (!isEmpty(compiledFile.data.standaloneCodeBlocks)) { + item.data.standaloneCodeBlocks = reduce( + compiledFile.data + .standaloneCodeBlocks as SourceData['standaloneCodeBlocks'], + (acc, blockSource, id) => ({ + ...acc, + [id]: normalizePath(blockSource), + }), + {}, + ); + } + } + + return item; + }); + + return paths; +}; diff --git a/site/packages/rd-vite/src/node/provider/nav.ts b/site/packages/rd-vite/src/node/provider/nav.ts new file mode 100644 index 000000000..851484a10 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/nav.ts @@ -0,0 +1,43 @@ +import { Navigators } from '@casts/rd-vite/types/navigator'; +import { capitalize, find, has, last } from 'lodash-es'; + +import { Source, SourceMeta } from '../../types'; +import { parsePath } from '../../utils/path'; + +export const getNavFromSource = (source: Source) => { + const { paths } = parsePath(source); + + const metaNav: SourceMeta['nav'] = source.meta?.nav; + + const nav = { + ...metaNav, + title: source.meta?.nav?.title || capitalize(last(paths)), + path: (metaNav?.path ? paths.slice(0, -1) : paths).join('/'), + }; + + return nav; +}; + +export type GetNavigatorsPayload = { + sources: Source[]; +}; + +export const getNavigators = ({ sources }: GetNavigatorsPayload) => { + const localesNav: Navigators = {}; + sources + .filter((s) => has(s.meta, 'nav')) + .forEach((source) => { + const { locale } = parsePath(source); + const navList = localesNav[locale] || (localesNav[locale] = []); + + const nav = getNavFromSource(source); + + if (nav.title && find(navList, { title: nav.title })) { + return; + } + + navList.push(nav); + }); + + return localesNav; +}; diff --git a/site/packages/rd-vite/src/node/provider/route.ts b/site/packages/rd-vite/src/node/provider/route.ts new file mode 100644 index 000000000..68fc6d782 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/route.ts @@ -0,0 +1,117 @@ +import { createElement, lazy } from 'react'; +import { find, isEmpty } from 'lodash-es'; +import { RouteObject } from 'react-router-dom'; + +import { RuntimeImport, Source } from '../../types'; +import { orderRoutes } from '../../utils/order-routes'; +import { ensureSlashStartPath, parsePath } from '../../utils/path'; +import { getNavFromSource } from './nav'; + +const NonLeafRouteOutlet = lazy(() => + import('../../client/components').then(({ NonLeafRouteOutlet }) => ({ + default: NonLeafRouteOutlet, + })), +); + +export type GenerateRoutesPayload = { + sources: Source[]; + runtimeImports: Record; +}; + +export const getRoutes = ({ + sources, + runtimeImports, +}: GenerateRoutesPayload) => { + const indexRE = /index$/; + const routes: RouteObject[] = []; + + sources.forEach((item: Source) => { + const { paths } = parsePath(item); + + let currentLevelRoutes = routes; + + paths.forEach((p, index) => { + const isLeaf = index === paths.length - 1; + const isIndex = indexRE.test(p); + + const absPath = isIndex + ? paths.slice(0, -1).join('/') || '/' + : paths.join('/'); + + const currentComponent = + runtimeImports[ensureSlashStartPath(item.path)].import; + + // NOTE: index route must be a leaf + if (isLeaf) { + const currentRoute: RouteObject = { + path: isIndex ? absPath : p, + element: createElement(lazy(currentComponent)), + index: isIndex, + meta: { + absPath, + ...item.meta, + }, + }; + + if (currentRoute.meta) { + currentRoute.meta.nav = getNavFromSource(item); + } + + currentLevelRoutes.push(currentRoute); + return; + } + + const currentRoute: RouteObject | undefined = currentLevelRoutes.find( + (r) => r.path === (p || '/'), + ); + const children: RouteObject[] = currentRoute?.children || []; + + if (!currentRoute) { + const currentRoute: RouteObject = { + path: p || '/', + children, + element: createElement(NonLeafRouteOutlet), + }; + + currentLevelRoutes.push(currentRoute); + } + + // go to next level + currentLevelRoutes = children; + }); + + /* --------------------------------- generate standalone demo routes ---------------------------------------- */ + if (!isEmpty(item.data.standaloneCodeBlocks)) { + const STANDALONE_CODE_BLOCK_ROUTE_PREFIX = '/~demo'; + + let standaloneCodeBlockRoute: RouteObject = find(routes, { + path: STANDALONE_CODE_BLOCK_ROUTE_PREFIX, + }) as RouteObject; + + if (!standaloneCodeBlockRoute) { + standaloneCodeBlockRoute = { + path: STANDALONE_CODE_BLOCK_ROUTE_PREFIX, + children: [], + }; + + routes.push(standaloneCodeBlockRoute); + } + + Object.values(item.data.standaloneCodeBlocks).forEach((blockSource) => { + const currentRoute: RouteObject = { + path: blockSource + .replace('.tsx', '') + .replace(/(\.)+\/?/g, '') + .replace(/\//g, '-'), + element: createElement('span', {}, 'test'), + meta: item.meta, + }; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + standaloneCodeBlockRoute.children!.push(currentRoute); + }); + } + }); + + return orderRoutes(routes); +}; diff --git a/site/packages/rd-vite/src/node/provider/sitemap.ts b/site/packages/rd-vite/src/node/provider/sitemap.ts new file mode 100644 index 000000000..49a48a0e5 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/sitemap.ts @@ -0,0 +1,101 @@ +import { find, isEmpty } from 'lodash-es'; +import { RouteObject } from 'react-router-dom'; + +import { ResolveFunction, Source } from '../../types'; +import { parsePath } from '../../utils'; +import { orderRoutes } from '../../utils/order-routes'; +import { getNavFromSource } from './nav'; + +export type DetectMarkdownsOptions = { + withMeta?: boolean; + resolve?: ResolveFunction; +}; + +export const generateRoutes = ({ sources }: { sources: Source[] }) => { + const indexRE = /index$/; + const routes: RouteObject[] = []; + + sources.forEach((item: Source) => { + const { paths } = parsePath(item); + + let currentLevelRoutes = routes; + + paths.forEach((p, index) => { + const isLeaf = index === paths.length - 1; + const isIndex = indexRE.test(p); + + const absPath = isIndex + ? paths.slice(0, -1).join('/') || '/' + : paths.join('/'); + + // NOTE: index route must be a leaf + if (isLeaf) { + const currentRoute: RouteObject = { + path: isIndex ? absPath : p, + index: isIndex, + meta: { + absPath, + ...item.meta, + }, + }; + + if (currentRoute.meta) { + currentRoute.meta.nav = getNavFromSource(item); + } + + currentLevelRoutes.push(currentRoute); + return; + } + + const currentRoute: RouteObject | undefined = currentLevelRoutes.find( + (r) => r.path === (p || '/'), + ); + const children: RouteObject[] = currentRoute?.children || []; + + if (!currentRoute) { + const currentRoute: RouteObject = { + path: p || '/', + children, + }; + + currentLevelRoutes.push(currentRoute); + } + + // go to next level + currentLevelRoutes = children; + }); + + /* --------------------------------- generate standalone demo routes ---------------------------------------- */ + if (!isEmpty(item.data.standaloneCodeBlocks)) { + const STANDALONE_CODE_BLOCK_ROUTE_PREFIX = '/~demo'; + + let standaloneCodeBlockRoute: RouteObject = find(routes, { + path: STANDALONE_CODE_BLOCK_ROUTE_PREFIX, + }) as RouteObject; + + if (!standaloneCodeBlockRoute) { + standaloneCodeBlockRoute = { + path: STANDALONE_CODE_BLOCK_ROUTE_PREFIX, + children: [], + }; + + routes.push(standaloneCodeBlockRoute); + } + + Object.values(item.data.standaloneCodeBlocks).forEach((blockSource) => { + const currentRoute: RouteObject = { + path: blockSource + .replace('.tsx', '') + .replace(/(\.)+\/?/g, '') + .replace(/\//g, '-'), + meta: item.meta, + }; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + standaloneCodeBlockRoute.children!.push(currentRoute); + }); + } + }); + + return orderRoutes(routes); +}; diff --git a/site/packages/rd-vite/src/node/provider/virtual.ts b/site/packages/rd-vite/src/node/provider/virtual.ts new file mode 100644 index 000000000..068a14348 --- /dev/null +++ b/site/packages/rd-vite/src/node/provider/virtual.ts @@ -0,0 +1,32 @@ +import { getNavigators } from '@casts/rd-vite/node/provider/nav'; +import { getRoutes } from '@casts/rd-vite/node/provider/route'; +import { Source } from '@casts/rd-vite/types'; + +export type InitRdProviderPayload = { + sources: Source[]; + runtimeImports: any; + /** + * Site name + */ + name?: string; +}; + +export const initRdProvider = ({ + sources, + runtimeImports, + ...rest +}: InitRdProviderPayload) => { + const routes = getRoutes({ + sources, + runtimeImports, + }); + + const navigators = getNavigators({ sources }); + + return { + ...rest, + sources, + routes, + navigators, + }; +}; diff --git a/site/packages/rd-vite/src/node/rehypes/index.ts b/site/packages/rd-vite/src/node/rehypes/index.ts new file mode 100644 index 000000000..0d2702f37 --- /dev/null +++ b/site/packages/rd-vite/src/node/rehypes/index.ts @@ -0,0 +1 @@ +export * from './toc'; diff --git a/site/packages/rd-vite/src/node/rehypes/toc.ts b/site/packages/rd-vite/src/node/rehypes/toc.ts new file mode 100644 index 000000000..cff978cf2 --- /dev/null +++ b/site/packages/rd-vite/src/node/rehypes/toc.ts @@ -0,0 +1,18 @@ +import rehypeTocBase from '@jsdevtools/rehype-toc'; +import { Options } from '@jsdevtools/rehype-toc/lib/options'; +import type { Processor, Transformer } from 'unified'; +import { matter } from 'vfile-matter'; + +export function rehypeToc(this: Processor, opts?: Options): Transformer { + const tocTransformer = rehypeTocBase.bind(this)(opts); + + return (ast, file) => { + const frontmatter = matter(file); + // @ts-ignore hide toc if matter.toc === false + if (frontmatter.data.matter?.toc === false) { + return ast; + } + // @ts-ignore toc transformer expected one argument + return tocTransformer(ast); + }; +} diff --git a/site/packages/rd-vite/src/node/remarks/code-block-replacer.ts b/site/packages/rd-vite/src/node/remarks/code-block-replacer.ts new file mode 100644 index 000000000..06052baa1 --- /dev/null +++ b/site/packages/rd-vite/src/node/remarks/code-block-replacer.ts @@ -0,0 +1,19 @@ +import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; +import type { Plugin, Transformer } from 'unified'; +import { visit } from 'unist-util-visit'; + +export const remarkCodeBlockReplacer: Plugin<[], MdxJsxFlowElement> = + (): Transformer => (ast) => { + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'code', + }, + (node: MdxJsxFlowElement) => { + node.name = 'Code'; + }, + ); + + return ast; + }; diff --git a/site/packages/rd-vite/src/node/remarks/code-block.ts b/site/packages/rd-vite/src/node/remarks/code-block.ts new file mode 100644 index 000000000..e22e883dd --- /dev/null +++ b/site/packages/rd-vite/src/node/remarks/code-block.ts @@ -0,0 +1,323 @@ +import { Node, parseSync, traverse } from '@babel/core'; +import { isIdentifier, isStringLiteral } from '@babel/types'; +import { parse } from 'acorn'; +import { Loader, transformSync } from 'esbuild'; +import { Program } from 'estree'; +import { existsSync, readFileSync } from 'fs'; +import { resolve as polyfillResolve } from 'import-meta-resolve'; +import { find, isEmpty, isString, map, zipObject } from 'lodash-es'; +import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; +import path, { extname, relative } from 'path'; +import type { Plugin, Transformer } from 'unified'; +import { visit } from 'unist-util-visit'; +import { pathToFileURL } from 'url'; + +import { ResolverRef } from '../../types'; +import { ensureSlashStartPath } from '../../utils'; +import { ensureRelativePath } from '../../utils/path'; + +export const jsExtRegex = /\.(tsx?|jsx?)$/; + +/** check is js file */ +export const isJsFile = (path: string) => /(.m?tsx?|.m?jsx?)$/.test(path); + +export const isNodeModulePackage = (path?: string) => + path && (path.includes('node_modules') || path.includes('dist')); + +/** parse code to ast using @babel/parser */ +export const parseFileSync = ( + path: string, +): ReturnType | void => { + const code = readFileSync(path).toString(); + const ext = jsExtRegex.exec(path)?.[1] || 'tsx'; + + try { + const transformResult = transformSync(code, { + loader: ext as Loader, + }); + return parseSync(transformResult.code); + } catch (err) { + console.log('failed to parseSync:', path); + } +}; + +export const collectImportSourcesFromAst = (ast: Node) => { + const sourcesSet = new Set(); + + if (ast) { + traverse(ast, { + ImportDeclaration: (path) => { + sourcesSet.add(path.node.source.value); + }, + CallExpression: (path) => { + if ( + // @ts-ignore ignore + isIdentifier(path.node.callee) && + path.node.callee.name === 'require' && + // @ts-ignore ignore + isStringLiteral(path.node.arguments[0]) + ) { + sourcesSet.add(path.node.arguments[0].value); + } + }, + }); + } + + return Array.from(sourcesSet); +}; + +export const ensureJsExt = (source: string) => { + const jsExts = [ + '.tsx', + '.ts', + '.jsx', + '.js', + '.mts', + '.mtsx', + '.mjs', + '.mjsx', + ]; + const currentExt = extname(source).replace('.tsx', ''); + + if (currentExt) { + return source; + } + + for (const ext of jsExts) { + const fullSource = `${source}${ext}`; + if (existsSync(fullSource)) { + return fullSource; + } + } + + return source; +}; + +export const remarkCodeBlock: Plugin< + [ + { + resolverRef?: ResolverRef; + root?: string; + }, + ], + MdxJsxFlowElement +> = (options): Transformer => { + const { resolverRef, root } = options; + + const resolve = + resolverRef?.current ?? + (async (source, importer) => { + const href = pathToFileURL(importer!).href; + return { + id: polyfillResolve(source, href), + }; + }); + + const getSlashStartPath = (p: string) => { + if (!root) { + return p; + } + + return ensureSlashStartPath(relative(root, p)); + }; + + return async (ast, file) => { + const codeAbsSources: string[] = []; + + /* --------------------------------- collect code sources start ---------------------------------------- */ + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'Code', + }, + (node: MdxJsxFlowElement) => { + const codeSrc: string = node.attributes?.[0].value as string; + + const codeAbsSrc = ensureJsExt(path.join(file.dirname!, codeSrc)); + + codeAbsSources.push(codeAbsSrc); + }, + ); + /* --------------------------------- collect code sources start ---------------------------------------- */ + + /* --------------------------------- load code sources content start ---------------------------------------- */ + const analyzeImports = async (indexPath: string) => { + const deps = new Set(); + + const traverseFileImports = async (currentPath: string) => { + const fileImportSources: Record = {}; + + if (!isJsFile(currentPath)) { + return fileImportSources; + } + + const ast = parseFileSync(currentPath); + + const importSources = collectImportSourcesFromAst(ast as Node); + + const resolveSource = async (source: string) => { + try { + const resolvedId = (await resolve(source, currentPath))?.id; + if (!resolvedId) { + return; + } + + if (isNodeModulePackage(resolvedId) || !source.startsWith('.')) { + deps.add(source); + return; + } + + const id = ensureRelativePath( + path.relative(currentPath, path.join(currentPath, source)), + ); + + fileImportSources[id] = resolvedId; + } catch (error) { + console.log(`cannot resolve ${source} from ${currentPath}`, error); + } + }; + + await Promise.all(importSources.map(resolveSource)); + + await Promise.all( + Object.values(fileImportSources).map(async (importSource) => { + Object.assign( + fileImportSources, + await traverseFileImports(importSource), + ); + return; + }), + ); + + return fileImportSources; + }; + + return await traverseFileImports(indexPath); + }; + + const importSources = zipObject( + codeAbsSources, + await Promise.all( + codeAbsSources.map(async (src) => { + return await analyzeImports(src); + }), + ), + ); + /* --------------------------------- load code sources content end ---------------------------------------- */ + + /* --------------------------------- pass code sources content to component start ---------------------------------------- */ + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'Code', + }, + (node: MdxJsxFlowElement) => { + const codeSrc = node.attributes?.[0].value; + if (!isString(codeSrc)) { + return; + } + + const codeAbsSrc = ensureJsExt(path.join(file.dirname!, codeSrc)); + const currentFileImportSources = importSources[codeAbsSrc]; + + const wholeImportSources = { + index: codeAbsSrc, + ...currentFileImportSources, + }; + + const importSourcesCode = `Object.assign({ + ${map(wholeImportSources, (source, id) => { + return `'${id}': () => import('${getSlashStartPath( + source, + )}?raw')`; + }).join(',')} + })`; + + const code = importSourcesCode; + const codeAst = parse(code, { ecmaVersion: 'latest' }); + + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'codeSources', + value: { + type: 'mdxJsxAttributeValueExpression', + value: code, + data: { + estree: codeAst as unknown as Program, + }, + }, + }); + + const childrenImportCode = `() => import('${getSlashStartPath( + codeAbsSrc, + )}')`; + const childrenCodeAst = parse(childrenImportCode, { + ecmaVersion: 'latest', + }); + + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'children', + value: { + type: 'mdxJsxAttributeValueExpression', + value: childrenImportCode, + data: { + estree: childrenCodeAst as unknown as Program, + }, + }, + }); + }, + ); + /* --------------------------------- pass code sources content to component end ---------------------------------------- */ + + return ast; + }; +}; + +export const remarkCodeBlockStandalone: Plugin< + [], + MdxJsxFlowElement +> = (): Transformer => { + return (ast, file) => { + const standaloneCodeBlocks: Record = {}; + + /* --------------------------------- collect standalone code block sources start ---------------------------------------- */ + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'Code', + }, + (node: MdxJsxFlowElement) => { + // NOTE: create standalone route anyway + const isStandalone = true; + // find( + // node.attributes, + // (attribute: MdxJsxAttribute) => attribute.name === 'iframe', + // ); + + if (!isStandalone) { + return; + } + + const source = find(node.attributes, { name: 'src' })?.value as string; + if (!source) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const codeBlockSrc = ensureJsExt(path.join(file.dirname!, source)); + standaloneCodeBlocks[source as string] = codeBlockSrc; + }, + ); + /* --------------------------------- collect standalone code block sources end ---------------------------------------- */ + + if (!isEmpty(standaloneCodeBlocks)) { + file.data.standaloneCodeBlocks = standaloneCodeBlocks; + } + + return ast; + }; +}; diff --git a/site/packages/rd-vite/src/node/remarks/index.ts b/site/packages/rd-vite/src/node/remarks/index.ts new file mode 100644 index 000000000..d184e5b25 --- /dev/null +++ b/site/packages/rd-vite/src/node/remarks/index.ts @@ -0,0 +1,3 @@ +export * from './code-block'; +export * from './code-block-replacer'; +export * from './react-api'; diff --git a/site/packages/rd-vite/src/node/remarks/react-api.ts b/site/packages/rd-vite/src/node/remarks/react-api.ts new file mode 100644 index 000000000..4aee2e409 --- /dev/null +++ b/site/packages/rd-vite/src/node/remarks/react-api.ts @@ -0,0 +1,278 @@ +import { + ApiDeclaration, + ApiDeclarations, + ApisDeclarations, + ResolveFunction, +} from '@casts/rd-vite/types'; +import * as acorn from 'acorn'; +import type { Program } from 'estree'; +import { + find, + isEmpty, + isUndefined, + map, + reduce, + uniq, + zipObject, +} from 'lodash-es'; +import type { BlockContent } from 'mdast'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'; +import { mdxJsxFromMarkdown } from 'mdast-util-mdx-jsx'; +import { MdxJsxFlowElement } from 'mdast-util-mdx-jsx/lib'; +import { gfmTable } from 'micromark-extension-gfm-table'; +import { mdxJsx } from 'micromark-extension-mdx-jsx'; +import { Acorn } from 'micromark-extension-mdx-jsx/lib/syntax'; +import { + ComponentDoc, + ParserOptions, + PropItem, + withDefaultConfig, +} from 'react-docgen-typescript'; +import { Plugin, Transformer } from 'unified'; +import { visit } from 'unist-util-visit'; + +const apiParserOptions: ParserOptions = { + shouldExtractValuesFromUnion: true, + shouldExtractLiteralValuesFromEnum: true, + + propFilter: (prop: PropItem) => { + if (prop.declarations !== undefined && prop.declarations.length > 0) { + const hasPropAdditionalDescription = prop.declarations.find( + (declaration) => { + return !declaration.fileName.includes('node_modules'); + }, + ); + + return Boolean(hasPropAdditionalDescription); + } + + return true; + }, +}; + +const apiParser = withDefaultConfig(apiParserOptions); + +export const remarkReactApi: Plugin< + [ + { + resolve: ResolveFunction; + }, + ], + MdxJsxFlowElement +> = ({ resolve }): Transformer => { + return async (ast) => { + const reactApiEntries: string[] = []; + + /* --------------------------------- collect api sources start ---------------------------------------- */ + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'API', + }, + (node: MdxJsxFlowElement) => { + const src = find(node.attributes, { name: 'src' }); + + const { value } = src as any; + + reactApiEntries.push(value); + }, + ); + /* --------------------------------- collect api sources end ---------------------------------------- */ + + /* --------------------------------- api docgen parse start ---------------------------------------- */ + const getReactApiRealPaths = async (entries: string[]) => { + const uniqueEntries = uniq(entries); + + return zipObject( + uniqueEntries, + await Promise.all( + uniqueEntries.map(async (entry) => { + const resolved = await resolve(entry); + + return resolved?.id || ''; + }), + ), + ); + }; + + const reactApiRealPaths = await getReactApiRealPaths(reactApiEntries); + + if (isEmpty(reactApiRealPaths)) { + return; + } + + const apiDeclarations = reduce( + reactApiRealPaths, + (acc, realPath, entry) => ({ + ...acc, + [entry]: apiParser.parse(realPath), + }), + {} as Record, + ); + /* --------------------------------- api docgen parse end ---------------------------------------- */ + + visit( + ast, + { + type: 'mdxJsxFlowElement', + name: 'API', + }, + (node: MdxJsxFlowElement, index: number, parent: MdxJsxFlowElement) => { + const src = find(node.attributes, { name: 'src' }); + + const { value } = src as any; + + const currentApiDeclarations = apiDeclarations[value]; + const apiData = getApiData(currentApiDeclarations); + + const apiDeclarationsCode = JSON.stringify(apiData); + + const apiDeclarationsAst = acorn.parse( + `Object.assign({}, ${apiDeclarationsCode})`, + { + ecmaVersion: 'latest', + }, + ); + + node.attributes.push({ + type: 'mdxJsxAttribute', + name: 'apis', + value: { + type: 'mdxJsxAttributeValueExpression', + value: apiDeclarationsCode, + data: { + estree: apiDeclarationsAst as unknown as Program, + }, + }, + }); + + const apisMarkdown = map(apiData, (apis, name) => { + return `### ${name}\n${getApiTableMarkdown(apis)}`; + }).join('\n'); + + const apsAst = fromMarkdown(apisMarkdown, { + extensions: [ + mdxJsx({ acorn: acorn as unknown as Acorn, addResult: true }), + gfmTable(), + ], + mdastExtensions: [mdxJsxFromMarkdown(), gfmTableFromMarkdown()], + }); + + // replace tag with apis + parent.children.splice( + index, + 1, + ...(apsAst.children as BlockContent[]), + ); + }, + ); + }; +}; + +const getApiData = (declarations: ComponentDoc[]) => { + const apis = reduce( + declarations, + (acc, componentDoc) => { + const props = map(componentDoc.props, (prop) => ({ + identifier: prop.name, + description: prop.description, + default: prop.defaultValue?.value, + type: prop.type, + required: prop.required, + })) as ApiDeclarations; + + return { + ...acc, + [componentDoc.displayName]: props, + }; + }, + {} as ApisDeclarations, + ); + + return apis; +}; + +const getApiTableMarkdown = (apis: ApiDeclarations) => { + let code = ''; + + const getDeclarationType = (type: ApiDeclaration['type']) => { + if (type.name === 'enum') { + // exclude some special enum + if ( + ['Color', 'boolean'].includes(type.raw) || + type.raw.includes('ReactNode') + ) { + return escape(`\`${type.raw}\``); + } + + return escape( + map( + type.value, + (item) => `\`${item.value.replace(/['"]/g, '')}\``, + ).join(' | '), + ); + } + + return escape(type.name); + }; + + /* --------------------------------- table head code ---------------------------------------- */ + code += '| Property | Description | Type | Default | Required |\n'; + code += '| --- | --- | --- | --- | --- |\n'; + + /* --------------------------------- table content code ---------------------------------------- */ + code += apis + .map( + (api) => + `| ${api.identifier} | ${(api.description || '-').replace( + /\n/g, + '
', + )} | ${getDeclarationType(api.type)} | ${ + isUndefined(api.default) ? '-' : escape(api.default) + } | ${api.required} |`, + ) + .join('\n'); + + return code; +}; + +const entityEscapeMap: Record = { + '<': '<', + '|': '\\|', +}; + +const codeEntityEscapeMap: Record = { + // prettier-ignore + // eslint-disable-next-line prettier/prettier, no-useless-escape + '<': '\<', + '|': '\\|', +}; + +const entityEscapeReg = RegExp( + '[' + Object.keys(entityEscapeMap).join('') + ']', + 'g', +); + +const codeEntityEscapeReg = RegExp( + '`[^`]*`|([' + Object.keys(codeEntityEscapeMap).join('') + '])', + 'g', +); + +// escape html tag +function escape(html: string) { + if (typeof html !== 'string') { + return ''; + } + + return html.replace(codeEntityEscapeReg, (codeHtml, group?: string) => { + if (group) { + return entityEscapeMap[group]; + } + + return codeHtml.replace(entityEscapeReg, function (match) { + return codeEntityEscapeMap[match]; + }); + }); +} diff --git a/site/packages/rd-vite/src/types/api.ts b/site/packages/rd-vite/src/types/api.ts new file mode 100644 index 000000000..bcab33c64 --- /dev/null +++ b/site/packages/rd-vite/src/types/api.ts @@ -0,0 +1,11 @@ +export type ApiDeclaration = { + identifier: string; + description: string; + default?: any; + type: any; + required: boolean; +}; + +export type ApiDeclarations = ApiDeclaration[]; + +export type ApisDeclarations = Record; diff --git a/site/packages/rd-vite/src/types/import.ts b/site/packages/rd-vite/src/types/import.ts new file mode 100644 index 000000000..4cdb23bbb --- /dev/null +++ b/site/packages/rd-vite/src/types/import.ts @@ -0,0 +1,3 @@ +export type RuntimeImport = { + import: () => Promise; +}; diff --git a/site/packages/rd-vite/src/types/index.ts b/site/packages/rd-vite/src/types/index.ts new file mode 100644 index 000000000..a5bbb547a --- /dev/null +++ b/site/packages/rd-vite/src/types/index.ts @@ -0,0 +1,5 @@ +export * from './api'; +export * from './import'; +export * from './mdx'; +export * from './resolve'; +export * from './source'; diff --git a/site/packages/rd-vite/src/types/mdx.ts b/site/packages/rd-vite/src/types/mdx.ts new file mode 100644 index 000000000..952e7752b --- /dev/null +++ b/site/packages/rd-vite/src/types/mdx.ts @@ -0,0 +1,5 @@ +import { Parent } from 'unist'; + +export interface MDXNode extends Parent { + type: string; +} diff --git a/site/packages/rd-vite/src/types/navigator.ts b/site/packages/rd-vite/src/types/navigator.ts new file mode 100644 index 000000000..a892fc76b --- /dev/null +++ b/site/packages/rd-vite/src/types/navigator.ts @@ -0,0 +1,7 @@ +export type Navigator = { + title: string; + order?: number; + path: string; +}; + +export type Navigators = Record; diff --git a/site/packages/rd-vite/src/types/resolve.ts b/site/packages/rd-vite/src/types/resolve.ts new file mode 100644 index 000000000..d867084bb --- /dev/null +++ b/site/packages/rd-vite/src/types/resolve.ts @@ -0,0 +1,5 @@ +import { PluginContext } from 'rollup'; + +export type ResolveFunction = PluginContext['resolve']; + +export type ResolverRef = { current?: ResolveFunction }; diff --git a/site/packages/rd-vite/src/types/source.ts b/site/packages/rd-vite/src/types/source.ts new file mode 100644 index 000000000..ffa01293f --- /dev/null +++ b/site/packages/rd-vite/src/types/source.ts @@ -0,0 +1,25 @@ +export type SourceMeta = { + [key: string]: string | number | Record; +} & { + title?: string; + description?: string; + absPath?: string; + nav?: Record; + group?: Record; + layout?: 'page' | 'componentDoc'; + hero?: Record; + features?: Array>; +}; + +export type SourceData = { + standaloneCodeBlocks?: { + [key: string]: string; + }; +}; + +export type Source = { + id: string; + path: string; + meta?: SourceMeta; + data: SourceData; +}; diff --git a/site/packages/rd-vite/src/utils/array-insert-interval.ts b/site/packages/rd-vite/src/utils/array-insert-interval.ts new file mode 100644 index 000000000..c2aba66bd --- /dev/null +++ b/site/packages/rd-vite/src/utils/array-insert-interval.ts @@ -0,0 +1,16 @@ +export const arrayInsertInterval = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + arr: any[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + element: any, + interval = 0, +) => { + const newArr = [...arr]; + if (interval <= 0) { + return newArr; + } + for (let i = interval; i < newArr.length; i += interval + 1) { + newArr.splice(i, 0, element); + } + return newArr; +}; diff --git a/site/packages/rd-vite/src/utils/index.ts b/site/packages/rd-vite/src/utils/index.ts new file mode 100644 index 000000000..9f891ae3a --- /dev/null +++ b/site/packages/rd-vite/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './path'; +export * from './toc'; diff --git a/site/packages/rd-vite/src/utils/order-routes.ts b/site/packages/rd-vite/src/utils/order-routes.ts new file mode 100644 index 000000000..86bd94489 --- /dev/null +++ b/site/packages/rd-vite/src/utils/order-routes.ts @@ -0,0 +1,50 @@ +import { orderBy, reduce } from 'lodash-es'; +import { RouteObject } from 'react-router-dom'; + +export const orderRoutes = (routes: RouteObject[]) => { + const internalOrder = (routes: RouteObject[]) => { + const groupAndOrder = reduce( + routes, + (acc, route) => { + if (!route.meta?.group) { + return acc; + } + + const group = { + ...acc[route.meta.group.title], + ...route.meta.group, + }; + + return { + ...acc, + [route.meta?.group.title]: group, + }; + }, + {} as Record>, + ); + + const fillGroupOrder = (routes: RouteObject[]) => + routes.map((route) => { + const newRoute = { ...route }; + + if (newRoute.meta?.group) { + newRoute.meta.group.order = + groupAndOrder[newRoute.meta.group.title].order; + } + + return newRoute; + }); + + return orderBy(fillGroupOrder(routes), [ + 'meta.group.order', + 'meta.order', + ]).map((route) => { + if (route.children) { + route.children = internalOrder(route.children); + } + return route; + }); + }; + + return internalOrder(routes); +}; diff --git a/site/packages/rd-vite/src/utils/path.ts b/site/packages/rd-vite/src/utils/path.ts new file mode 100644 index 000000000..15f4b7442 --- /dev/null +++ b/site/packages/rd-vite/src/utils/path.ts @@ -0,0 +1,52 @@ +import { filter, identity, isEmpty } from 'lodash-es'; + +import { fallbackLocaleCode, localeCodes } from '../common'; + +export function ensureRelativePath(relativePath: string) { + // prefix . for same-level path + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}`; + } + return relativePath; +} + +export const ensureSlashStartPath = (path: string) => { + if (path.startsWith('/')) { + return path; + } + return `/${path}`; +}; + +/** + * parse source item + * @param {any} item - source item + */ +export const parsePath = (item: any) => { + const localesRE = new RegExp(`\\.(${localeCodes.join('|')})$`); + const routePathRE = new RegExp('(docs|src)([\\/\\w\\d\\-_\\.]+)?.md'); + const routePath = routePathRE.exec(item.id)?.[2] || ''; + + let paths = routePath.split('/'); + const namePath = paths[paths.length - 1]; + + paths = paths.slice(0, -1); + + const metaPath = (item.meta.nav?.path || '') + (item.meta.group?.path || ''); + + if (metaPath) { + const clearMetaPath = filter(metaPath.split('/'), identity); + + paths.splice(0, 0, ...clearMetaPath); + } + + const locale = localesRE.exec(namePath)?.[1]; + + paths.push(namePath.replace(localesRE, '')); + + paths = filter(paths, (p) => !isEmpty(p)); + + const prefix = locale && locale !== fallbackLocaleCode ? `/${locale}` : ''; + paths.splice(0, 0, prefix); + + return { paths, locale: locale || fallbackLocaleCode }; +}; diff --git a/site/packages/rd-vite/src/utils/toc.ts b/site/packages/rd-vite/src/utils/toc.ts new file mode 100644 index 000000000..245acda6d --- /dev/null +++ b/site/packages/rd-vite/src/utils/toc.ts @@ -0,0 +1,104 @@ +import { HtmlElementNode } from '@jsdevtools/rehype-toc'; +import { parse } from 'acorn'; +import { Program } from 'estree'; +import { identity, isEmpty } from 'lodash-es'; + +export type TocItem = { + text: string; + href: string; + children?: TocItem[]; +}; + +export const getTocData = (node: HtmlElementNode) => { + const findTocRoot = (node?: HtmlElementNode): HtmlElementNode | undefined => { + if (!node) { + return undefined; + } + + if (node?.properties?.className?.includes('toc-level')) { + return node; + } + + if (isEmpty(node?.children)) { + return undefined; + } + + return findTocRoot(node.children?.[0] as HtmlElementNode); + }; + + const rootLevel = findTocRoot(node); + + const transformLevel = (level?: HtmlElementNode): TocItem[] | undefined => { + if (!level?.children) { + return; + } + + const nodes = (level.children as HtmlElementNode[]).map( + (item: HtmlElementNode) => { + const anchor = item.children?.[0] as HtmlElementNode | undefined; + if (!anchor) { + return undefined; + } + + const child: TocItem = { + // @ts-ignore value will exist + title: anchor.children?.[0]?.value, + href: anchor.properties.href!, + }; + + const childLevel = item.children?.[1] as HtmlElementNode; + + if (childLevel) { + child.children = transformLevel(childLevel); + } + + return child; + }, + ); + + const cleanNodes = nodes.filter(identity) as TocItem[]; + + return isEmpty(cleanNodes) ? undefined : cleanNodes; + }; + + return transformLevel(rootLevel); +}; + +export const getTocDataAst = (toc: HtmlElementNode) => { + const data = getTocData(toc); + + const dataAst = parse(`Object.assign(${JSON.stringify(data)})`, { + ecmaVersion: 'latest', + }); + + return dataAst; +}; + +export const customizeTOC = (toc: HtmlElementNode) => { + const data = getTocData(toc) || []; + + const dataCode = `${JSON.stringify(data)}`; + + const dataAst = parse(dataCode, { + ecmaVersion: 'latest', + }); + + Object.assign(toc, { + type: 'mdxJsxFlowElement', + name: 'Toc', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'data', + value: { + type: 'mdxJsxAttributeValueExpression', + value: dataCode, + data: { + estree: dataAst as unknown as Program, + }, + }, + }, + ], + }); + return toc; +}; diff --git a/site/prerender.js b/site/prerender.js new file mode 100644 index 000000000..85cc8b825 --- /dev/null +++ b/site/prerender.js @@ -0,0 +1,66 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +import { render } from './dist/server/entry-server.js'; + +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = dirname(__filename); // get the name of the directory + +const OUT_DIR = join(__dirname, './dist'); + +const prerender = async () => { + const indexTemplate = readFileSync(join(OUT_DIR, './index.html'), 'utf-8'); + + const routes = JSON.parse( + readFileSync(join(OUT_DIR, './routes-manifest.json'), 'utf-8'), + ); + + const generate = async (path, route) => { + try { + const result = await render(path); + + const { html } = result; + + let finalHtml = indexTemplate.replace('', html); + + const regex = /(])*>)(.*?)(<\/title>)/; + const titleMatch = regex.exec(indexTemplate); + if (titleMatch) { + const title = [route?.meta?.title, titleMatch[2]] + .filter(Boolean) + .join(' - '); + + finalHtml = finalHtml.replace(regex, `$1${title}$3`); + } + + const writeFileDir = join(OUT_DIR, `.${path}`); + const writeFilePath = join(writeFileDir, '', 'index.html'); + + mkdirSync(writeFileDir, { recursive: true }); + + writeFileSync(writeFilePath, finalHtml); + } catch (error) { + console.log('[ERROR] generate failed: ', { + route: path, + error, + }); + } + }; + + const dfs = (routes) => { + for (const route of routes) { + if (route?.meta?.absPath) { + generate(route.meta.absPath, route); + } + + if (route.children && route.children?.length > 0) { + dfs(route.children); + } + } + }; + + dfs(routes); +}; + +prerender(); diff --git a/site/public/favicon.svg b/site/public/favicon.svg new file mode 100644 index 000000000..399348b59 --- /dev/null +++ b/site/public/favicon.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/site/src/App.tsx b/site/src/App.tsx new file mode 100644 index 000000000..b4d5c7918 --- /dev/null +++ b/site/src/App.tsx @@ -0,0 +1,11 @@ +import { RdProvider } from '@casts/rd-vite/client/components'; + +import { App as BaseApp } from '../packages/rd-theme/src'; + +const App = () => ( + + + +); + +export default App; diff --git a/site/src/assets/favicon.svg b/site/src/assets/favicon.svg new file mode 100644 index 000000000..399348b59 --- /dev/null +++ b/site/src/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/site/src/assets/home-banner.svg b/site/src/assets/home-banner.svg new file mode 100644 index 000000000..1e26ecb7d --- /dev/null +++ b/site/src/assets/home-banner.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/src/assets/react.svg b/site/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/site/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/src/brand.svg b/site/src/brand.svg new file mode 100644 index 000000000..7846866b8 --- /dev/null +++ b/site/src/brand.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/site/src/entry-client.tsx b/site/src/entry-client.tsx new file mode 100644 index 000000000..c444becf0 --- /dev/null +++ b/site/src/entry-client.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import App from './App'; + +// ReactDOM.hydrateRoot( +// document.getElementById('root') as HTMLElement, +// +// +// +// +// , +// ); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + , +); diff --git a/site/src/entry-server.tsx b/site/src/entry-server.tsx new file mode 100644 index 000000000..73eeca582 --- /dev/null +++ b/site/src/entry-server.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { RdProvider } from '@casts/rd-vite/client/components'; +import ReactDOMServer from 'react-dom/server'; +import { StaticRouter } from 'react-router-dom/server'; +import { Writable } from 'stream'; + +import App from './App'; + +class HtmlWritable extends Writable { + chunks: Buffer[] = []; + html = ''; + + getHtml() { + return this.html; + } + + _write(chunk: Buffer, _encoding: BufferEncoding, callback: () => void) { + this.chunks.push(chunk); + callback(); + } + + _final(callback: () => void) { + this.html = this.chunks.map((chunk) => chunk.toString()).join(' '); + callback(); + } +} + +export async function render(location: string) { + const html = await new Promise((resolve, reject) => { + const htmlWritable = new HtmlWritable(); + + const stream = ReactDOMServer.renderToPipeableStream( + + + + + + + , + { + onAllReady() { + stream.pipe(htmlWritable); + }, + onError(error) { + reject(error); + }, + }, + ); + + htmlWritable.on('finish', () => { + resolve(htmlWritable.getHtml()); + }); + }); + + return { html }; +} diff --git a/site/src/main.tsx b/site/src/main.tsx new file mode 100644 index 000000000..51d2d8e9a --- /dev/null +++ b/site/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; + +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + , +); diff --git a/site/src/vite-env.d.ts b/site/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/site/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/site/tsconfig.json b/site/tsconfig.json new file mode 100644 index 000000000..0595f8e80 --- /dev/null +++ b/site/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": ["typings.d.ts", "typings/react-router-dom.d.ts"], + "extends": "../tsconfig.json", + "include": ["src", "packages", "../docs"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@casts/rd-vite/*": ["packages/rd-vite/src/*"], + "@casts/rd-theme/*": ["packages/rd-theme/src/*"] + } + } + // "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/site/tsconfig.node.json b/site/tsconfig.node.json new file mode 100644 index 000000000..50f2b3910 --- /dev/null +++ b/site/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "packages"] +} diff --git a/site/typings.d.ts b/site/typings.d.ts new file mode 100644 index 000000000..3999f2dab --- /dev/null +++ b/site/typings.d.ts @@ -0,0 +1,14 @@ +declare module 'virtual:rd-provider' { + import { Navigators } from '@casts/rd-vite/types/navigator'; + import { RouteObject } from 'react-router-dom'; + + import { Source } from './packages/rd-vite/src/types'; + + export const rdProvider: { + sources: Source; + navigators: Navigators; + routes: RouteObject[]; + }; +} + +declare module '*.mdx'; diff --git a/site/typings/react-router-dom.d.ts b/site/typings/react-router-dom.d.ts new file mode 100644 index 000000000..4a0b3a990 --- /dev/null +++ b/site/typings/react-router-dom.d.ts @@ -0,0 +1,14 @@ +// eslint-disable-next-line unused-imports/no-unused-imports +import { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; + +import { SourceMeta } from '../packages/rd-vite/src/types'; + +declare module 'react-router-dom' { + interface IndexRouteObject { + meta?: SourceMeta; + } + + interface NonIndexRouteObject { + meta?: SourceMeta; + } +} diff --git a/site/vite.config.ts b/site/vite.config.ts new file mode 100644 index 000000000..50db7d65a --- /dev/null +++ b/site/vite.config.ts @@ -0,0 +1,59 @@ +import vitestConfig from '@casts/standard/dist/cjs/vite/vite.config'; +import react from '@vitejs/plugin-react'; +import { omit } from 'lodash-es'; +import path from 'path'; +import { defineConfig } from 'vite'; +import { mergeConfig } from 'vite'; +import svgr from 'vite-plugin-svgr'; + +import { getPackageAlias } from '../utils'; +import { rd } from './packages/rd-vite/src'; + +const packagesAlias = omit( + getPackageAlias(path.resolve(path.join(__dirname, '../packages'))), + ['@casts/theme/dist/esm', '@casts/theme/dist/cjs', '@casts/theme'], +); + +export default mergeConfig( + vitestConfig, + defineConfig({ + server: { + host: '0.0.0.0', + port: 15000, + open: '/', + https: false, + }, + + resolve: { + alias: { + '@': path.resolve(__dirname, '..'), + ...packagesAlias, + }, + }, + + // css: { + // preprocessorOptions: { + // scss: { + // additionalData: '$prefix-cls: rdc;\n', + // }, + // }, + // }, + + plugins: [ + react(), + svgr(), + rd({ + name: 'Casts Design', + // docRoot: path.resolve(__dirname, '..'), + }), + ], + + optimizeDeps: { + include: ['@casts/icons'], + exclude: ['@casts/config-provider'], + esbuildOptions: { + jsx: 'automatic', + }, + }, + }), +);