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
/**
*
* <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,
markdown: 'prettier',
},
extends: [
'plugin:@docusaurus/recommended',
],
})
+1
View File
@@ -28,6 +28,7 @@
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.3",
"@docusaurus/eslint-plugin": "^3.9.2",
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
+107
View File
@@ -33,6 +33,9 @@ importers:
'@antfu/eslint-config':
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)
'@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':
specifier: 3.9.2
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==}
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':
resolution: {integrity: sha512-/SVCc57ByARzGSU60c50rMyQlBuMIJCjcsJlkphxY6B0GV4UH3tcA1994N8fFfbJ9kX3jIBe/xg3XP5qBtGDbA==}
engines: {node: '>=20.0'}
@@ -1799,6 +1808,9 @@ packages:
'@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@types/send@0.17.6':
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
@@ -1850,6 +1862,10 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1867,16 +1883,35 @@ packages:
eslint: ^8.57.0 || ^9.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':
resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==}
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':
resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
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':
resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1884,6 +1919,10 @@ packages:
eslint: ^8.57.0 || ^9.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':
resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -5588,9 +5627,18 @@ packages:
ts-pattern@5.9.0:
resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.8.1:
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:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -7354,6 +7402,15 @@ snapshots:
postcss-sort-media-queries: 5.2.0(postcss@8.5.6)
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':
dependencies:
chalk: 4.1.2
@@ -8572,6 +8629,8 @@ snapshots:
dependencies:
'@types/node': 17.0.45
'@types/semver@7.7.1': {}
'@types/send@0.17.6':
dependencies:
'@types/mime': 1.3.5
@@ -8646,6 +8705,11 @@ snapshots:
transitivePeerDependencies:
- 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':
dependencies:
'@typescript-eslint/types': 8.52.0
@@ -8667,8 +8731,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@5.62.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)':
dependencies:
'@typescript-eslint/project-service': 8.52.0(typescript@5.6.3)
@@ -8684,6 +8764,21 @@ snapshots:
transitivePeerDependencies:
- 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)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
@@ -8695,6 +8790,11 @@ snapshots:
transitivePeerDependencies:
- 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':
dependencies:
'@typescript-eslint/types': 8.52.0
@@ -13052,8 +13152,15 @@ snapshots:
ts-pattern@5.9.0: {}
tslib@1.14.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:
dependencies:
prelude-ls: 1.2.1
@@ -1,18 +1,18 @@
import type { ReactNode } from "react";
import clsx from "clsx";
import Heading from "@theme/Heading";
import styles from "./styles.module.css";
import type { ReactNode } from 'react'
import Heading from '@theme/Heading'
import clsx from 'clsx'
import styles from './styles.module.css'
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<"svg">>;
description: ReactNode;
};
interface FeatureItem {
title: string
Svg: React.ComponentType<React.ComponentProps<'svg'>>
description: ReactNode
}
const FeatureList: FeatureItem[] = [
{
title: "Easy to Use",
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
title: 'Easy to Use',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
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",
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
title: 'Focus on What Matters',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
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",
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
title: 'Powered by React',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
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) {
return (
<div className={clsx("col col--4")}>
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
@@ -53,7 +57,7 @@ function Feature({ title, Svg, description }: FeatureItem) {
<p>{description}</p>
</div>
</div>
);
)
}
export default function HomepageFeatures(): ReactNode {
@@ -67,5 +71,5 @@ export default function HomepageFeatures(): ReactNode {
</div>
</div>
</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} />
}