This commit is contained in:
oneao committed 2026-01-06 21:49:19 +08:00
1 parent ad87788261
commit 17e07df91a
12 files changed
+604 -26

No files matched your search

-6
View File
@@ -138,9 +138,3 @@ const config: Config = {
} }
export default config export default config
/**
*
* <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12ZM16 12V13.5C16 14.8807 17.1193 16 18.5 16V16C19.8807 16 21 14.8807 21 13.5V12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21H16" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
*
*/
+3
View File
@@ -7,4 +7,7 @@ export default antfu({
html: true, html: true,
markdown: 'prettier', markdown: 'prettier',
}, },
extends: [
'plugin:@docusaurus/recommended',
],
}) })
+1
View File
@@ -28,6 +28,7 @@
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^6.7.3", "@antfu/eslint-config": "^6.7.3",
"@docusaurus/eslint-plugin": "^3.9.2",
"@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2", "@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2", "@docusaurus/types": "3.9.2",
+107
View File
@@ -33,6 +33,9 @@ importers:
'@antfu/eslint-config': '@antfu/eslint-config':
specifier: ^6.7.3 specifier: ^6.7.3
version: 6.7.3(@eslint-react/eslint-plugin@2.5.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3))(@vue/compiler-sfc@3.5.26)(eslint-plugin-format@1.2.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3) version: 6.7.3(@eslint-react/eslint-plugin@2.5.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3))(@vue/compiler-sfc@3.5.26)(eslint-plugin-format@1.2.0(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@1.21.7)))(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)
'@docusaurus/eslint-plugin':
specifier: ^3.9.2
version: 3.9.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)
'@docusaurus/module-type-aliases': '@docusaurus/module-type-aliases':
specifier: 3.9.2 specifier: 3.9.2
version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 3.9.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -1164,6 +1167,12 @@ packages:
resolution: {integrity: sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==} resolution: {integrity: sha512-8gBKup94aGttRduABsj7bpPFTX7kbwu+xh3K9NMCF5K4bWBqTFYW+REKHF6iBVDHRJ4grZdIPbvkiHd/XNKRMQ==}
engines: {node: '>=20.0'} engines: {node: '>=20.0'}
'@docusaurus/eslint-plugin@3.9.2':
resolution: {integrity: sha512-LnCrmrR4EtzpSiq6aoSfiY0Lf8P0WslGbBFZJ0olKXJIMxey8dpKevT1K/+tN87Lbn2H/VrdGGSPGlfVKmihAQ==}
engines: {node: '>=20.0'}
peerDependencies:
eslint: '>=6'
'@docusaurus/logger@3.9.2': '@docusaurus/logger@3.9.2':
resolution: {integrity: sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==} resolution: {integrity: sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==}
engines: {node: '>=20.0'} engines: {node: '>=20.0'}
@@ -1799,6 +1808,9 @@ packages:
'@types/sax@1.2.7': '@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@types/send@0.17.6': '@types/send@0.17.6':
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
@@ -1850,6 +1862,10 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@5.62.0':
resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/scope-manager@8.52.0': '@typescript-eslint/scope-manager@8.52.0':
resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==} resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1867,16 +1883,35 @@ packages:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@5.62.0':
resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/types@8.52.0': '@typescript-eslint/types@8.52.0':
resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@5.62.0':
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
'@typescript-eslint/typescript-estree@8.52.0': '@typescript-eslint/typescript-estree@8.52.0':
resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies: peerDependencies:
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@5.62.0':
resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
'@typescript-eslint/utils@8.52.0': '@typescript-eslint/utils@8.52.0':
resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==} resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1884,6 +1919,10 @@ packages:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0' typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@5.62.0':
resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@typescript-eslint/visitor-keys@8.52.0': '@typescript-eslint/visitor-keys@8.52.0':
resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5588,9 +5627,18 @@ packages:
ts-pattern@5.9.0: ts-pattern@5.9.0:
resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
peerDependencies:
typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta'
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -7354,6 +7402,15 @@ snapshots:
postcss-sort-media-queries: 5.2.0(postcss@8.5.6) postcss-sort-media-queries: 5.2.0(postcss@8.5.6)
tslib: 2.8.1 tslib: 2.8.1
'@docusaurus/eslint-plugin@3.9.2(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)':
dependencies:
'@typescript-eslint/utils': 5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)
eslint: 9.39.2(jiti@1.21.7)
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
- typescript
'@docusaurus/logger@3.9.2': '@docusaurus/logger@3.9.2':
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2
@@ -8572,6 +8629,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 17.0.45 '@types/node': 17.0.45
'@types/semver@7.7.1': {}
'@types/send@0.17.6': '@types/send@0.17.6':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
@@ -8646,6 +8705,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/scope-manager@5.62.0':
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
'@typescript-eslint/scope-manager@8.52.0': '@typescript-eslint/scope-manager@8.52.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.52.0 '@typescript-eslint/types': 8.52.0
@@ -8667,8 +8731,24 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/types@5.62.0': {}
'@typescript-eslint/types@8.52.0': {} '@typescript-eslint/types@8.52.0': {}
'@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.3)':
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
debug: 4.4.3
globby: 11.1.0
is-glob: 4.0.3
semver: 7.7.3
tsutils: 3.21.0(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.52.0(typescript@5.6.3)': '@typescript-eslint/typescript-estree@8.52.0(typescript@5.6.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.52.0(typescript@5.6.3) '@typescript-eslint/project-service': 8.52.0(typescript@5.6.3)
@@ -8684,6 +8764,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@5.62.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
'@types/json-schema': 7.0.15
'@types/semver': 7.7.1
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3)
eslint: 9.39.2(jiti@1.21.7)
eslint-scope: 5.1.1
semver: 7.7.3
transitivePeerDependencies:
- supports-color
- typescript
'@typescript-eslint/utils@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)': '@typescript-eslint/utils@8.52.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.6.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -8695,6 +8790,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/visitor-keys@5.62.0':
dependencies:
'@typescript-eslint/types': 5.62.0
eslint-visitor-keys: 3.4.3
'@typescript-eslint/visitor-keys@8.52.0': '@typescript-eslint/visitor-keys@8.52.0':
dependencies: dependencies:
'@typescript-eslint/types': 8.52.0 '@typescript-eslint/types': 8.52.0
@@ -13052,8 +13152,15 @@ snapshots:
ts-pattern@5.9.0: {} ts-pattern@5.9.0: {}
tslib@1.14.1: {}
tslib@2.8.1: {} tslib@2.8.1: {}
tsutils@3.21.0(typescript@5.6.3):
dependencies:
tslib: 1.14.1
typescript: 5.6.3
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -1,18 +1,18 @@
import type { ReactNode } from "react"; import type { ReactNode } from 'react'
import clsx from "clsx"; import Heading from '@theme/Heading'
import Heading from "@theme/Heading"; import clsx from 'clsx'
import styles from "./styles.module.css"; import styles from './styles.module.css'
type FeatureItem = { interface FeatureItem {
title: string; title: string
Svg: React.ComponentType<React.ComponentProps<"svg">>; Svg: React.ComponentType<React.ComponentProps<'svg'>>
description: ReactNode; description: ReactNode
}; }
const FeatureList: FeatureItem[] = [ const FeatureList: FeatureItem[] = [
{ {
title: "Easy to Use", title: 'Easy to Use',
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: ( description: (
<> <>
Docusaurus was designed from the ground up to be easily installed and used to get your Docusaurus was designed from the ground up to be easily installed and used to get your
@@ -21,18 +21,22 @@ const FeatureList: FeatureItem[] = [
), ),
}, },
{ {
title: "Focus on What Matters", title: 'Focus on What Matters',
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: ( description: (
<> <>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go ahead and move your Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go ahead and move your
docs into the <code>docs</code> directory. docs into the
{' '}
<code>docs</code>
{' '}
directory.
</> </>
), ),
}, },
{ {
title: "Powered by React", title: 'Powered by React',
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: ( description: (
<> <>
Extend or customize your website layout by reusing React. Docusaurus can be extended while Extend or customize your website layout by reusing React. Docusaurus can be extended while
@@ -40,11 +44,11 @@ const FeatureList: FeatureItem[] = [
</> </>
), ),
}, },
]; ]
function Feature({ title, Svg, description }: FeatureItem) { function Feature({ title, Svg, description }: FeatureItem) {
return ( return (
<div className={clsx("col col--4")}> <div className={clsx('col col--4')}>
<div className="text--center"> <div className="text--center">
<Svg className={styles.featureSvg} role="img" /> <Svg className={styles.featureSvg} role="img" />
</div> </div>
@@ -53,7 +57,7 @@ function Feature({ title, Svg, description }: FeatureItem) {
<p>{description}</p> <p>{description}</p>
</div> </div>
</div> </div>
); )
} }
export default function HomepageFeatures(): ReactNode { export default function HomepageFeatures(): ReactNode {
@@ -67,5 +71,5 @@ export default function HomepageFeatures(): ReactNode {
</div> </div>
</div> </div>
</section> </section>
); )
} }
@@ -0,0 +1,68 @@
import React, {useState, useRef, useEffect} from 'react';
import clsx from 'clsx';
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
import NavbarItem from '@theme/NavbarItem';
export default function DropdownNavbarItemDesktop({
items,
position,
className,
onClick,
...props
}) {
const dropdownRef = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
useEffect(() => {
const handleClickOutside = (event) => {
if (!dropdownRef.current || dropdownRef.current.contains(event.target)) {
return;
}
setShowDropdown(false);
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
document.addEventListener('focusin', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
document.removeEventListener('focusin', handleClickOutside);
};
}, [dropdownRef]);
return (
<div
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--right': position === 'right',
'dropdown--show': showDropdown,
})}>
<NavbarNavLink
aria-haspopup="true"
aria-expanded={showDropdown}
role="button"
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
href={props.to ? undefined : '#'}
className={clsx('navbar__link', className)}
{...props}
onClick={props.to ? undefined : (e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setShowDropdown(!showDropdown);
}
}}>
{props.children ?? props.label}
</NavbarNavLink>
<ul className="dropdown__menu">
{items.map((childItemProps, i) => (
<NavbarItem
isDropdownItem
activeClassName="dropdown__link--active"
{...childItemProps}
key={i}
/>
))}
</ul>
</div>
);
}
@@ -0,0 +1,82 @@
import type { Props } from '@theme/NavbarItem/DropdownNavbarItem/Desktop'
import type { ReactNode } from 'react'
import NavbarItem from '@theme/NavbarItem'
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'
import clsx from 'clsx'
import React, { useEffect, useRef, useState } from 'react'
export default function DropdownNavbarItemDesktop({
items,
position,
className,
onClick,
...props
}: Props): ReactNode {
const dropdownRef = useRef<HTMLDivElement>(null)
const [showDropdown, setShowDropdown] = useState(false)
useEffect(() => {
const handleClickOutside = (
event: MouseEvent | TouchEvent | FocusEvent,
) => {
if (
!dropdownRef.current
|| dropdownRef.current.contains(event.target as Node)
) {
return
}
setShowDropdown(false)
}
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
document.addEventListener('focusin', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
document.removeEventListener('focusin', handleClickOutside)
}
}, [dropdownRef])
return (
<div
ref={dropdownRef}
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
'dropdown--right': position === 'right',
'dropdown--show': showDropdown,
})}
>
<NavbarNavLink
aria-haspopup="true"
aria-expanded={showDropdown}
role="button"
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
href={props.to ? undefined : '#'}
className={clsx('navbar__link', className)}
{...props}
onClick={props.to ? undefined : e => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
setShowDropdown(!showDropdown)
}
}}
>
{props.children ?? props.label}
</NavbarNavLink>
<ul className="dropdown__menu">
{items.map((childItemProps, i) => (
<NavbarItem
isDropdownItem
activeClassName="dropdown__link--active"
{...childItemProps}
key={i}
/>
))}
</ul>
</div>
)
}
@@ -0,0 +1,136 @@
import React, {useEffect} from 'react';
import clsx from 'clsx';
import {
isRegexpStringMatch,
useCollapsible,
Collapsible,
} from '@docusaurus/theme-common';
import {isSamePath, useLocalPathname} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
import NavbarItem from '@theme/NavbarItem';
import styles from './styles.module.css';
function isItemActive(item, localPathname) {
if (isSamePath(item.to, localPathname)) {
return true;
}
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
return true;
}
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
return true;
}
return false;
}
function containsActiveItems(items, localPathname) {
return items.some((item) => isItemActive(item, localPathname));
}
function CollapseButton({collapsed, onClick}) {
return (
<button
aria-label={
collapsed
? translate({
id: 'theme.navbar.mobileDropdown.collapseButton.expandAriaLabel',
message: 'Expand the dropdown',
description:
'The ARIA label of the button to expand the mobile dropdown navbar item',
})
: translate({
id: 'theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel',
message: 'Collapse the dropdown',
description:
'The ARIA label of the button to collapse the mobile dropdown navbar item',
})
}
aria-expanded={!collapsed}
type="button"
className="clean-btn menu__caret"
onClick={onClick}
/>
);
}
function useItemCollapsible({active}) {
const {collapsed, toggleCollapsed, setCollapsed} = useCollapsible({
initialState: () => !active,
});
// Expand if any item active after a navigation
useEffect(() => {
if (active) {
setCollapsed(false);
}
}, [active, setCollapsed]);
return {
collapsed,
toggleCollapsed,
};
}
export default function DropdownNavbarItemMobile({
items,
className,
position, // Need to destructure position from props so that it doesn't get passed on.
onClick,
...props
}) {
const localPathname = useLocalPathname();
const isActive = isSamePath(props.to, localPathname);
const containsActive = containsActiveItems(items, localPathname);
const {collapsed, toggleCollapsed} = useItemCollapsible({
active: isActive || containsActive,
});
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
const href = props.to ? undefined : '#';
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}>
<div
className={clsx('menu__list-item-collapsible', {
'menu__list-item-collapsible--active': isActive,
})}>
<NavbarNavLink
role="button"
className={clsx(
styles.dropdownNavbarItemMobile,
'menu__link menu__link--sublist',
className,
)}
href={href}
{...props}
onClick={(e) => {
// Prevent navigation when link is "#"
if (href === '#') {
e.preventDefault();
}
// Otherwise we let navigation eventually happen, and/or collapse
toggleCollapsed();
}}>
{props.children ?? props.label}
</NavbarNavLink>
<CollapseButton
collapsed={collapsed}
onClick={(e) => {
e.preventDefault();
toggleCollapsed();
}}
/>
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
{items.map((childItemProps, i) => (
<NavbarItem
mobile
isDropdownItem
onClick={onClick}
activeClassName="menu__link--active"
{...childItemProps}
key={i}
/>
))}
</Collapsible>
</li>
);
}
@@ -0,0 +1,159 @@
import React, {useEffect, type ReactNode, type ComponentProps} from 'react';
import clsx from 'clsx';
import {
isRegexpStringMatch,
useCollapsible,
Collapsible,
} from '@docusaurus/theme-common';
import {isSamePath, useLocalPathname} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
import NavbarItem, {type LinkLikeNavbarItemProps} from '@theme/NavbarItem';
import type {Props} from '@theme/NavbarItem/DropdownNavbarItem/Mobile';
import styles from './styles.module.css';
function isItemActive(
item: LinkLikeNavbarItemProps,
localPathname: string,
): boolean {
if (isSamePath(item.to, localPathname)) {
return true;
}
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
return true;
}
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
return true;
}
return false;
}
function containsActiveItems(
items: readonly LinkLikeNavbarItemProps[],
localPathname: string,
): boolean {
return items.some((item) => isItemActive(item, localPathname));
}
function CollapseButton({
collapsed,
onClick,
}: {
collapsed: boolean;
onClick: ComponentProps<'button'>['onClick'];
}) {
return (
<button
aria-label={
collapsed
? translate({
id: 'theme.navbar.mobileDropdown.collapseButton.expandAriaLabel',
message: 'Expand the dropdown',
description:
'The ARIA label of the button to expand the mobile dropdown navbar item',
})
: translate({
id: 'theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel',
message: 'Collapse the dropdown',
description:
'The ARIA label of the button to collapse the mobile dropdown navbar item',
})
}
aria-expanded={!collapsed}
type="button"
className="clean-btn menu__caret"
onClick={onClick}
/>
);
}
function useItemCollapsible({active}: {active: boolean}) {
const {collapsed, toggleCollapsed, setCollapsed} = useCollapsible({
initialState: () => !active,
});
// Expand if any item active after a navigation
useEffect(() => {
if (active) {
setCollapsed(false);
}
}, [active, setCollapsed]);
return {
collapsed,
toggleCollapsed,
};
}
export default function DropdownNavbarItemMobile({
items,
className,
position, // Need to destructure position from props so that it doesn't get passed on.
onClick,
...props
}: Props): ReactNode {
const localPathname = useLocalPathname();
const isActive = isSamePath(props.to, localPathname);
const containsActive = containsActiveItems(items, localPathname);
const {collapsed, toggleCollapsed} = useItemCollapsible({
active: isActive || containsActive,
});
// # hash permits to make the <a> tag focusable in case no link target
// See https://github.com/facebook/docusaurus/pull/6003
// There's probably a better solution though...
const href = props.to ? undefined : '#';
return (
<li
className={clsx('menu__list-item', {
'menu__list-item--collapsed': collapsed,
})}>
<div
className={clsx('menu__list-item-collapsible', {
'menu__list-item-collapsible--active': isActive,
})}>
<NavbarNavLink
role="button"
className={clsx(
styles.dropdownNavbarItemMobile,
'menu__link menu__link--sublist',
className,
)}
href={href}
{...props}
onClick={(e) => {
// Prevent navigation when link is "#"
if (href === '#') {
e.preventDefault();
}
// Otherwise we let navigation eventually happen, and/or collapse
toggleCollapsed();
}}>
{props.children ?? props.label}
</NavbarNavLink>
<CollapseButton
collapsed={collapsed}
onClick={(e) => {
e.preventDefault();
toggleCollapsed();
}}
/>
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
{items.map((childItemProps, i) => (
<NavbarItem
mobile
isDropdownItem
onClick={onClick}
activeClassName="menu__link--active"
{...childItemProps}
key={i}
/>
))}
</Collapsible>
</li>
);
}
@@ -0,0 +1,3 @@
.dropdownNavbarItemMobile {
cursor: pointer;
}
@@ -0,0 +1,8 @@
import DropdownNavbarItemDesktop from '@theme/NavbarItem/DropdownNavbarItem/Desktop'
import DropdownNavbarItemMobile from '@theme/NavbarItem/DropdownNavbarItem/Mobile'
import React from 'react'
export default function DropdownNavbarItem({ mobile = false, ...props }) {
const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop
return <Comp {...props} />
}
@@ -0,0 +1,13 @@
import type { Props } from '@theme/NavbarItem/DropdownNavbarItem'
import type { ReactNode } from 'react'
import DropdownNavbarItemDesktop from '@theme/NavbarItem/DropdownNavbarItem/Desktop'
import DropdownNavbarItemMobile from '@theme/NavbarItem/DropdownNavbarItem/Mobile'
import React from 'react'
export default function DropdownNavbarItem({
mobile = false,
...props
}: Props): ReactNode {
const Comp = mobile ? DropdownNavbarItemMobile : DropdownNavbarItemDesktop
return <Comp {...props} />
}