diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 7644d15..4d77fd8 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -10,5 +10,10 @@ export const Button = ( props: TProps & HTMLProps, ): JSX.Element => { const { children, className, ...rest } = props - return + + return ( + + ) } diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index 037f17e..703bc84 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -15,6 +15,8 @@ import LinkedinSvg from '@site/static/icons/linkedin.svg' import DiscourseSvg from '@site/static/icons/discourse.svg' import TelegramSvg from '@site/static/icons/telegram.svg' import StatusSvg from '@site/static/icons/status.svg' +import SearchSvg from '@site/static/icons/search.svg' +import DropdownSvg from '@site/static/icons/dropdown.svg' type TIconProps = { size?: 's' | 'm' | 'l' @@ -113,3 +115,15 @@ export const IconFolderFilled = (props: TIconProps): JSX.Element => ( ) + +export const IconSearch = (props: TIconProps): JSX.Element => ( + + + +) + +export const IconDropdown = (props: TIconProps): JSX.Element => ( + + + +) diff --git a/src/css/custom.scss b/src/css/custom.scss index 23aa753..e591cea 100644 --- a/src/css/custom.scss +++ b/src/css/custom.scss @@ -251,9 +251,6 @@ small { .footer__logo { margin: 0 !important; } -.menu_src-theme-Navbar-Content-styles-module svg { - display: none; -} .card { padding: var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal); diff --git a/src/theme/DocSidebarItem/Category/index.tsx b/src/theme/DocSidebarItem/Category/index.tsx new file mode 100644 index 0000000..3486fae --- /dev/null +++ b/src/theme/DocSidebarItem/Category/index.tsx @@ -0,0 +1,215 @@ +import React, { type ComponentProps, useEffect, useMemo } from 'react' +import clsx from 'clsx' +import { + ThemeClassNames, + useThemeConfig, + usePrevious, + Collapsible, + useCollapsible, +} from '@docusaurus/theme-common' +import { + isActiveSidebarItem, + findFirstCategoryLink, + useDocSidebarItemsExpandedState, + isSamePath, +} from '@docusaurus/theme-common/internal' +import Link from '@docusaurus/Link' +import { translate } from '@docusaurus/Translate' +import useIsBrowser from '@docusaurus/useIsBrowser' +import DocSidebarItems from '@theme/DocSidebarItems' +import type { Props } from '@theme/DocSidebarItem/Category' +import { + IconArrowLeftCircle, + IconArrowRightCircle, + IconDropdown, +} from '@site/src/components/Icon' + +// If we navigate to a category and it becomes active, it should automatically +// expand itself +function useAutoExpandActiveCategory({ + isActive, + collapsed, + updateCollapsed, +}: { + isActive: boolean + collapsed: boolean + updateCollapsed: (b: boolean) => void +}) { + const wasActive = usePrevious(isActive) + useEffect(() => { + const justBecameActive = isActive && !wasActive + if (justBecameActive && collapsed) { + updateCollapsed(false) + } + }, [isActive, wasActive, collapsed, updateCollapsed]) +} + +/** + * When a collapsible category has no link, we still link it to its first child + * during SSR as a temporary fallback. This allows to be able to navigate inside + * the category even when JS fails to load, is delayed or simply disabled + * React hydration becomes an optional progressive enhancement + * see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188 + * see https://github.com/facebook/docusaurus/issues/3030 + */ +function useCategoryHrefWithSSRFallback( + item: Props['item'], +): string | undefined { + const isBrowser = useIsBrowser() + return useMemo(() => { + if (item.href) { + return item.href + } + // In these cases, it's not necessary to render a fallback + // We skip the "findFirstCategoryLink" computation + if (isBrowser || !item.collapsible) { + return undefined + } + return findFirstCategoryLink(item) + }, [item, isBrowser]) +} + +function CollapseButton({ + categoryLabel, + onClick, +}: { + categoryLabel: string + onClick: ComponentProps<'button'>['onClick'] +}) { + return ( + + + + ) +} diff --git a/src/theme/Navbar/Content/MobileSearch/index.ts b/src/theme/Navbar/Content/MobileSearch/index.ts new file mode 100644 index 0000000..251136d --- /dev/null +++ b/src/theme/Navbar/Content/MobileSearch/index.ts @@ -0,0 +1 @@ +export * from './MobileSearch' diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx index 98032b6..9a1119f 100644 --- a/src/theme/Navbar/Content/index.tsx +++ b/src/theme/Navbar/Content/index.tsx @@ -1,5 +1,10 @@ import { useThemeConfig } from '@docusaurus/theme-common' -import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal' +import { + useNavbarMobileSidebar, + useSearchPage, +} from '@docusaurus/theme-common/internal' +import { IconSearch } from '@site/src/components/Icon' +import { IconButton } from '@site/src/components/IconButton' import { globalStore, selectHiddenSidebar, @@ -12,7 +17,9 @@ import NavbarSearch from '@theme/Navbar/Search' import NavbarItem, { Props as NavbarItemConfig } from '@theme/NavbarItem' import SearchBar from '@theme/SearchBar' import clsx from 'clsx' -import React from 'react' +import React, { useState } from 'react' +import { useSearch } from '../../SearchBar/hooks/useSearch' +import { MobileSearch } from './MobileSearch/MobileSearch' import { ShareButton } from './ShareButton' import styles from './styles.module.scss' @@ -59,8 +66,16 @@ export default function NavbarContent(): JSX.Element { const dispatch = globalStore.useDispatch() const hiddenDesktopSidebar = globalStore.useSelector(selectHiddenSidebar) + const [mobileSearch, setMobileSearch] = useState(false) + return ( -
+
-
@@ -108,6 +123,34 @@ export default function NavbarContent(): JSX.Element { )}
+ +
+ {localeDropdown && } + setMobileSearch(true)} + > + + +
+ +
+ setMobileSearch(false)} + /> +
) } diff --git a/src/theme/Navbar/Content/styles.module.scss b/src/theme/Navbar/Content/styles.module.scss index 9e32a17..50f18fc 100644 --- a/src/theme/Navbar/Content/styles.module.scss +++ b/src/theme/Navbar/Content/styles.module.scss @@ -116,16 +116,15 @@ .root > * { width: auto; - flex: 0 0 auto !important; + flex: 0 0 auto; padding: 0; } - .headerLeft { + .root > div:first-child { + flex: 0 0 auto; } .headerMiddle { - margin-left: 20px; - .leftContainer.shifted { transform: none; } @@ -138,6 +137,55 @@ .headerRight { display: none !important; } + + .searchContainer { + display: none; + } +} + +.headerRightMobile { + visibility: visible; + display: flex !important; + + flex: 1 1 auto !important; + + align-items: center; + justify-content: flex-end; + + & > div { + display: initial; + } + + & > div:first-child { + border-radius: 12px; + padding: 4px 12px; + height: 2.125rem; + + a { + font-size: 0.875rem; + } + + svg { + margin-left: 0.75rem; + vertical-align: middle; + } + + margin-right: 0.75rem; + } + + .searchButton { + background-color: #eeeef1; + padding: 7px 13px; + border-radius: 12px; + } + + transition: all 0.8s, opacity 0.4s; + transform: translateX(0%); + + &.shifted { + transform: translateX(-100%); + opacity: 0%; + } } /* @@ -159,4 +207,65 @@ Hide color mode toggle in small viewports .sidebarButton.desktop { display: none; } + + .root > div:first-child, + .headerMiddle { + transition: 1s; + } +} + +.root { + & > div:first-child, + .headerMiddle { + opacity: 1; + visibility: visible; + } +} + +.root.activeMobileSearch { + & > div:first-child, + .headerMiddle { + opacity: 0; + visibility: hidden; + } +} + +.mobileSearchContainer { + top: 0; + left: 0; + width: 100% !important; + height: 100%; + position: absolute; + + background-color: var(--ifm-background-surface-color); + transform: translateX(100%); + opacity: 0; + transition: 0.6s ease-in-out; + visibility: hidden; + + &.visible { + opacity: 1; + transform: translateX(0%); + visibility: visible; + } +} + +.headerRightMobile { + display: none !important; + visibility: hidden; + + flex-grow: 1 !important; +} + +@include utils.responsive('lg', 'down') { + .headerRightMobile { + display: flex !important; + visibility: visible; + } +} + +.links { + a svg { + display: none !important; + } } diff --git a/src/theme/Navbar/MobileSidebar/Header/index.tsx b/src/theme/Navbar/MobileSidebar/Header/index.tsx new file mode 100644 index 0000000..2698d84 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/Header/index.tsx @@ -0,0 +1,32 @@ +import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal' +import { IconArrowLeftCircle } from '@site/src/components/Icon' +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle' +import NavbarLogo from '@theme/Navbar/Logo' +import React from 'react' + +function CloseButton() { + const mobileSidebar = useNavbarMobileSidebar() + return ( + + ) +} + +export default function NavbarMobileSidebarHeader(): JSX.Element { + return ( +
+
+ + +
+ +
+ ) +} diff --git a/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx b/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx new file mode 100644 index 0000000..59f35a4 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx @@ -0,0 +1,40 @@ +import React, { type ComponentProps } from 'react' +import { useThemeConfig } from '@docusaurus/theme-common' +import { useNavbarSecondaryMenu } from '@docusaurus/theme-common/internal' +import Translate from '@docusaurus/Translate' +import { ButtonWithIcon } from '@site/src/components/Button/ButtonWithIcon' +import { IconArrowLeftCircle } from '@site/src/components/Icon' +import styles from './styles.module.scss' + +function SecondaryMenuBackButton(props: ComponentProps<'button'>) { + return ( + } + {...props} + > + + Main Menu + + + ) +} + +// The secondary menu slides from the right and shows contextual information +// such as the docs sidebar +export default function NavbarMobileSidebarSecondaryMenu(): JSX.Element | null { + const isPrimaryMenuEmpty = useThemeConfig().navbar.items.length === 0 + const secondaryMenu = useNavbarSecondaryMenu() + return ( + <> + {/* edge-case: prevent returning to the primaryMenu when it's empty */} + {!isPrimaryMenuEmpty && ( + secondaryMenu.hide()} /> + )} + {secondaryMenu.content} + + ) +} diff --git a/src/theme/Navbar/MobileSidebar/SecondaryMenu/styles.module.scss b/src/theme/Navbar/MobileSidebar/SecondaryMenu/styles.module.scss new file mode 100644 index 0000000..6937a4e --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/SecondaryMenu/styles.module.scss @@ -0,0 +1,18 @@ +.backButton { + & > div { + flex-direction: row-reverse; + + color: #373738; + font-size: 0.875rem; + + svg { + * { + stroke: #909091; + } + } + } + + border: 1px solid #eeeef1; + padding: 12px 18px; + margin-bottom: 8px; +} diff --git a/src/theme/Navbar/index.scss b/src/theme/Navbar/index.scss index cd1c7d7..c03137b 100644 --- a/src/theme/Navbar/index.scss +++ b/src/theme/Navbar/index.scss @@ -1,3 +1,5 @@ +@use '@site/src/css/utils'; + .navbar-sidebar__brand { --ifm-navbar-padding-horizontal: 1rem; } @@ -11,5 +13,58 @@ background: none; } -:root { +@include utils.responsive('lg', 'down') { + .navbar { + background: var(--ifm-background-surface-color); + } +} + +.overflow-hidden { + overflow: hidden !important; +} + +.navbar-sidebar__item.menu, +.theme-doc-sidebar-menu { + li a, + .theme-doc-sidebar-item-category a { + padding: 0.75rem 1.125rem; + border-radius: 0.75rem; + + font-size: 0.875rem; + color: #373738; + + .menu__link--active { + background-color: #eeeef0; + } + } + + .menu__list-item-collapsible { + border-radius: 0.75rem; + + a::after { + display: none; + } + + .dropdown-icon { + position: absolute; + right: 1.125rem; + } + } + + li.menu__list-item { + .dropdown-icon { + transform: rotate(0deg); + transition: transform var(--ifm-transition-fast); + } + + &--collapsed { + .dropdown-icon { + transform: rotate(-90deg); + } + } + } + + li:not(:first-child) { + margin-top: 0.125rem; + } } diff --git a/src/theme/Navbar/index.tsx b/src/theme/Navbar/index.tsx index 05ffc9b..58844fd 100644 --- a/src/theme/Navbar/index.tsx +++ b/src/theme/Navbar/index.tsx @@ -1,12 +1,27 @@ -import React from 'react' +import { useNavbarMobileSidebar } from '@docusaurus/theme-common/internal' +import type { WrapperProps } from '@docusaurus/types' import Navbar from '@theme-original/Navbar' import type NavbarType from '@theme/Navbar' -import type { WrapperProps } from '@docusaurus/types' +import React, { useEffect } from 'react' import './index.scss' type Props = WrapperProps export default function NavbarWrapper(props: Props): JSX.Element { + const mobileSidebar = useNavbarMobileSidebar() + + useEffect(() => { + if (mobileSidebar.shown) { + document + .querySelector('#__docusaurus') + ?.classList?.add?.('overflow-hidden') + } else { + document + .querySelector('#__docusaurus') + ?.classList?.remove?.('overflow-hidden') + } + }, [mobileSidebar.shown]) + return ( <> diff --git a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx index 7d1229f..39b8844 100644 --- a/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx +++ b/src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx @@ -1,13 +1,10 @@ -import React from 'react' -import useDocusaurusContext from '@docusaurus/useDocusaurusContext' import { useAlternatePageUtils } from '@docusaurus/theme-common/internal' import { translate } from '@docusaurus/Translate' -import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem' -import IconLanguage from '@theme/Icon/Language' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' import type { LinkLikeNavbarItemProps } from '@theme/NavbarItem' +import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem' import type { Props } from '@theme/NavbarItem/LocaleDropdownNavbarItem' - -import styles from './styles.module.css' +import React from 'react' export default function LocaleDropdownNavbarItem({ mobile, @@ -59,7 +56,7 @@ export default function LocaleDropdownNavbarItem({ {dropdownLabel}} + label={<>{currentLocale.toUpperCase()}} items={items} /> ) diff --git a/src/theme/SearchBar/SearchBar.module.scss b/src/theme/SearchBar/SearchBar.module.scss index 6e91c9f..50ebe6d 100644 --- a/src/theme/SearchBar/SearchBar.module.scss +++ b/src/theme/SearchBar/SearchBar.module.scss @@ -1,3 +1,11 @@ +@use '@site/src/css/utils'; + .root { position: relative; } + +@include utils.responsive('lg', 'down') { + .root { + position: unset; + } +} diff --git a/src/theme/SearchBar/SearchInput/SearchInput.module.scss b/src/theme/SearchBar/SearchInput/SearchInput.module.scss index 8a3f8b3..6f27978 100644 --- a/src/theme/SearchBar/SearchInput/SearchInput.module.scss +++ b/src/theme/SearchBar/SearchInput/SearchInput.module.scss @@ -1,5 +1,7 @@ // TODO: use ifm variables +@use '@site/src/css/utils'; + .root { width: auto; position: relative; @@ -72,3 +74,27 @@ background-color: var(--ifm-background-color); } } + +@include utils.responsive('lg', 'down') { + .shortcuts { + display: none; + } + + .root { + width: 100%; + } + + .root input { + width: 100%; + border-radius: 12px; + background: #eeeef0; + } + + .root.expanded input { + width: 100%; + } + + .label { + display: none; + } +} diff --git a/src/theme/SearchBar/SearchInput/SearchInput.tsx b/src/theme/SearchBar/SearchInput/SearchInput.tsx index 2019719..d6803fc 100644 --- a/src/theme/SearchBar/SearchInput/SearchInput.tsx +++ b/src/theme/SearchBar/SearchInput/SearchInput.tsx @@ -2,7 +2,7 @@ import { useOS } from '@site/src/lib/useOS' import { useWindowEventListener } from '@site/src/lib/useWindowEventListener' import clsx from 'clsx' import React from 'react' -import {} from 'react-use' +import { useMedia } from 'react-use' import styles from './SearchInput.module.scss' export type SearchInputProps = Omit< @@ -30,6 +30,7 @@ export const SearchInput: React.FC = ({ ...props }) => { const os = useOS() + const isMobile = useMedia('(max-width: 996px)') const expanded = active || value?.length > 0 @@ -67,7 +68,7 @@ export const SearchInput: React.FC = ({ ref={inputRef} onFocus={onFocus} {...inputProps} - placeholder={expanded ? inputProps.placeholder : ''} + placeholder={expanded || isMobile ? inputProps.placeholder : ''} />
{active ? ( diff --git a/src/theme/SearchBar/SearchResultsContainer/SearchResultsContainer.module.scss b/src/theme/SearchBar/SearchResultsContainer/SearchResultsContainer.module.scss index e7219a4..7f0cf98 100644 --- a/src/theme/SearchBar/SearchResultsContainer/SearchResultsContainer.module.scss +++ b/src/theme/SearchBar/SearchResultsContainer/SearchResultsContainer.module.scss @@ -1,3 +1,5 @@ +@use '@site/src/css/utils'; + .root { background-color: var(--ifm-background-surface-color); border-radius: 1rem; @@ -19,3 +21,11 @@ visibility: visible; } } + +@include utils.responsive('lg', 'down') { + .root { + width: 100%; + margin-top: 0; + border-radius: 0; + } +} diff --git a/src/theme/TOCCollapsible/CollapseButton/index.tsx b/src/theme/TOCCollapsible/CollapseButton/index.tsx new file mode 100644 index 0000000..6aeb35b --- /dev/null +++ b/src/theme/TOCCollapsible/CollapseButton/index.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import clsx from 'clsx' +import Translate from '@docusaurus/Translate' +import type { Props } from '@theme/TOCCollapsible/CollapseButton' + +import styles from './styles.module.scss' +import { IconArrowLeftCircle } from '@site/src/components/Icon' + +export default function TOCCollapsibleCollapseButton({ + collapsed, + ...props +}: Props): JSX.Element { + return ( + + ) +} diff --git a/src/theme/TOCCollapsible/CollapseButton/styles.module.scss b/src/theme/TOCCollapsible/CollapseButton/styles.module.scss new file mode 100644 index 0000000..d061ee1 --- /dev/null +++ b/src/theme/TOCCollapsible/CollapseButton/styles.module.scss @@ -0,0 +1,29 @@ +.tocCollapsibleButton { + font-size: inherit; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0.75rem 1.125rem; +} + +.tocCollapsibleButton > div { + width: 12px; + height: 12px; + transform: translateY(-4px); + + svg { + * { + stroke: #909090; + } + + transform: rotate(270deg); + transition: transform var(--ifm-transition-fast); + } +} + +.tocCollapsibleButtonExpanded > div { + svg { + transform: rotate(90deg); + } +} diff --git a/src/theme/TOCCollapsible/index.tsx b/src/theme/TOCCollapsible/index.tsx new file mode 100644 index 0000000..0b3910a --- /dev/null +++ b/src/theme/TOCCollapsible/index.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import clsx from 'clsx' +import { useCollapsible, Collapsible } from '@docusaurus/theme-common' +import TOCItems from '@theme/TOCItems' +import CollapseButton from '@theme/TOCCollapsible/CollapseButton' +import type { Props } from '@theme/TOCCollapsible' + +import styles from './styles.module.css' + +export default function TOCCollapsible({ + toc, + className, + minHeadingLevel, + maxHeadingLevel, +}: Props): JSX.Element { + const { collapsed, toggleCollapsed } = useCollapsible({ + initialState: true, + }) + return ( +
+ + +
+
+
+ + +
+ ) +} diff --git a/src/theme/TOCCollapsible/styles.module.css b/src/theme/TOCCollapsible/styles.module.css new file mode 100644 index 0000000..fb915ee --- /dev/null +++ b/src/theme/TOCCollapsible/styles.module.css @@ -0,0 +1,44 @@ +.tocCollapsible { + background-color: transparent; + border-radius: var(--ifm-global-radius); + border: 1px solid #eeeef1; + margin: 1rem 0; +} + +.tocCollapsible, +.tocCollapsible * { + font-size: 0.875rem; + color: var(--ifm-black-color); +} + +.divider { + width: 100%; + padding: 0rem 1.125rem; +} + +.divider > div { + height: 1px; + background-color: #eeeef1; +} + +.tocCollapsibleContent > ul { + border-left: none; + padding: 0.75rem 1.125rem; + font-size: 15px; +} + +.tocCollapsibleContent ul li { + margin: 4px 0; +} + +.tocCollapsible ul > li li { + margin: 0 2.25rem; +} + +.tocCollapsibleContent a { + display: block; +} + +.tocCollapsibleExpanded { + transform: none; +} diff --git a/static/icons/search.svg b/static/icons/search.svg new file mode 100644 index 0000000..c92d829 --- /dev/null +++ b/static/icons/search.svg @@ -0,0 +1,10 @@ + + + + + + + + + +