From 865a239c6d77dc7eefc8c2d69c609566b90eaa29 Mon Sep 17 00:00:00 2001 From: Kuizuo Date: Sat, 15 Jun 2024 08:20:37 +0800 Subject: [PATCH] feat: enhance ui --- src/css/custom.css | 9 +- src/hooks/useReadPercent.ts | 2 +- src/theme/BlogPostGridItems/index.tsx | 2 +- src/theme/BlogPostItem/Container/index.tsx | 16 ++-- .../BlogPostItem/Container/styles.module.css | 34 ------- src/theme/BlogPostPage/index.tsx | 2 +- src/theme/Navbar/ColorModeToggle/index.tsx | 28 ++++++ .../Navbar/ColorModeToggle/styles.module.css | 3 + src/theme/Navbar/Content/index.tsx | 90 +++++++++++++++++++ src/theme/Navbar/Content/styles.module.css | 8 ++ src/theme/Navbar/Layout/index.tsx | 51 +++++++++++ src/theme/Navbar/Layout/styles.module.css | 7 ++ src/theme/Navbar/Logo/index.tsx | 12 +++ .../Navbar/MobileSidebar/Header/index.tsx | 33 +++++++ .../Navbar/MobileSidebar/Layout/index.tsx | 1 - .../MobileSidebar/PrimaryMenu/index.tsx | 31 +++++++ .../MobileSidebar/SecondaryMenu/index.tsx | 32 +++++++ .../Navbar/MobileSidebar/Toggle/index.tsx | 23 +++++ src/theme/Navbar/MobileSidebar/index.tsx | 26 ++++++ src/theme/Navbar/Search/index.tsx | 16 ++++ src/theme/Navbar/Search/styles.module.css | 21 +++++ src/theme/Navbar/index.tsx | 11 +++ src/theme/TOC/index.tsx | 12 ++- src/theme/TOC/styles.module.css | 1 - 24 files changed, 418 insertions(+), 53 deletions(-) delete mode 100644 src/theme/BlogPostItem/Container/styles.module.css create mode 100644 src/theme/Navbar/ColorModeToggle/index.tsx create mode 100644 src/theme/Navbar/ColorModeToggle/styles.module.css create mode 100644 src/theme/Navbar/Content/index.tsx create mode 100644 src/theme/Navbar/Content/styles.module.css create mode 100644 src/theme/Navbar/Layout/index.tsx create mode 100644 src/theme/Navbar/Layout/styles.module.css create mode 100644 src/theme/Navbar/Logo/index.tsx create mode 100644 src/theme/Navbar/MobileSidebar/Header/index.tsx create mode 100644 src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx create mode 100644 src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx create mode 100644 src/theme/Navbar/MobileSidebar/Toggle/index.tsx create mode 100644 src/theme/Navbar/MobileSidebar/index.tsx create mode 100644 src/theme/Navbar/Search/index.tsx create mode 100644 src/theme/Navbar/Search/styles.module.css create mode 100644 src/theme/Navbar/index.tsx diff --git a/src/css/custom.css b/src/css/custom.css index a0803b80..916b2b32 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -4,6 +4,7 @@ @layer base { :root { + --content-background: #fdfdfd; --ifm-color-primary: #12affa; --ifm-color-primary-dark: #05a1ec; --ifm-color-primary-darker: #0598df; @@ -16,6 +17,7 @@ --ifm-heading-font-family: ui-sans-serif, system-ui, -apple-system; + --ifm-navbar-background-color: var(--content-background); --ifm-navbar-shadow: 0 4px 28px rgb(0 0 0 / 10%); --ifm-menu-color: #0d203a; @@ -45,8 +47,6 @@ --ifm-border-color: #e5e7eb; --ifm-toc-border-color: var(--ifm-border-color); - --content-background: #fdfdfd; - --blog-item-background-color: linear-gradient(180deg, #fcfcfc, #fff); --blog-item-shadow: 0 10px 18px #f1f5f9dd, 0 0 10px 0 #e4e4e7dd; --blog-item-shade: #f4f4f5; @@ -57,6 +57,7 @@ } html[data-theme='dark'] { + --content-background: #18181b; --ifm-color-primary: hsl(214deg 100% 60%); --ifm-color-primary-light: hsl(214deg 100% 75%); --ifm-heading-color: hsl(0deg 0% 100%); @@ -64,9 +65,10 @@ --ifm-text-color: var(--ifm-menu-color); --ifm-secondary-text-color: #eee; --ifm-border-color: #313131; + + --ifm-navbar-background-color: var(--content-background); --ifm-toc-border-color: var(--ifm-border-color); - --content-background: #18181b; --blog-item-background-color: linear-gradient(180deg, #171717, #18181b); --blog-item-shadow: 0 10px 18px #25374833, 0 0 8px #25374866; --blog-item-shade: #27272a; @@ -174,7 +176,6 @@ article p > span > a { .navbar { box-shadow: none; - background-color: transparent; } .navbar-sidebar__items { diff --git a/src/hooks/useReadPercent.ts b/src/hooks/useReadPercent.ts index c6fe1268..35ab8ef3 100644 --- a/src/hooks/useReadPercent.ts +++ b/src/hooks/useReadPercent.ts @@ -10,7 +10,7 @@ export const useReadPercent = () => { postRef.current = document.getElementById('__blog-post-container') }, []) - useMotionValueEvent(scrollYProgress, 'change', latest => { + useMotionValueEvent(scrollYProgress, 'change', (latest) => { setScrollProgress(latest) }) diff --git a/src/theme/BlogPostGridItems/index.tsx b/src/theme/BlogPostGridItems/index.tsx index 3186ba54..27cb8865 100644 --- a/src/theme/BlogPostGridItems/index.tsx +++ b/src/theme/BlogPostGridItems/index.tsx @@ -38,7 +38,7 @@ export default function BlogPostGridItems({ items }: BlogPostItemsProps): JSX.El onMouseEnter={() => setHoveredIndex(idx)} onMouseLeave={() => setHoveredIndex(null)} > - + {hoveredIndex === idx && ( -
-
+
+
diff --git a/src/theme/BlogPostItem/Container/styles.module.css b/src/theme/BlogPostItem/Container/styles.module.css deleted file mode 100644 index e23c978d..00000000 --- a/src/theme/BlogPostItem/Container/styles.module.css +++ /dev/null @@ -1,34 +0,0 @@ -:root { - --border-color: hsla(240, 6%, 90%, 0.7); -} - -html[data-theme='dark'] { - --border-color: #262626; -} - -.article { - position: relative; - @apply px-4 pt-4 pb-3 lg:px-6; - /* border: 1px solid var(--border-color); */ -} - -.cover { - position: absolute; - z-index: 1; - left: 0; - right: 0; - top: 0; - height: 224px; -} - -.coverMask { - border-radius: var(--ifm-pagination-nav-border-radius); - - background-size: cover; - background-position: center; - background-repeat: no-repeat; - height: 100%; - width: 100%; - -webkit-mask-image: linear-gradient(180deg, #fff -17.19%, #00000000 92.43%); - mask-image: linear-gradient(180deg, #fff -17.19%, #00000000 92.43%); -} diff --git a/src/theme/BlogPostPage/index.tsx b/src/theme/BlogPostPage/index.tsx index 229b51da..63ed76a1 100644 --- a/src/theme/BlogPostPage/index.tsx +++ b/src/theme/BlogPostPage/index.tsx @@ -10,7 +10,7 @@ import type { Props } from '@theme/BlogPostPage' import BlogPostPageMetadata from '@theme/BlogPostPage/Metadata' import BlogPostPaginator from '@theme/BlogPostPaginator' import TOC from '@theme/TOC' -import React, { type ReactNode } from 'react' +import { type ReactNode } from 'react' function BlogPostPageContent({ sidebar, diff --git a/src/theme/Navbar/ColorModeToggle/index.tsx b/src/theme/Navbar/ColorModeToggle/index.tsx new file mode 100644 index 00000000..b0d40077 --- /dev/null +++ b/src/theme/Navbar/ColorModeToggle/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {useColorMode, useThemeConfig} from '@docusaurus/theme-common'; +import ColorModeToggle from '@theme/ColorModeToggle'; +import type {Props} from '@theme/Navbar/ColorModeToggle'; +import styles from './styles.module.css'; + +export default function NavbarColorModeToggle({ + className, +}: Props): JSX.Element | null { + const navbarStyle = useThemeConfig().navbar.style; + const disabled = useThemeConfig().colorMode.disableSwitch; + const {colorMode, setColorMode} = useColorMode(); + + if (disabled) { + return null; + } + + return ( + + ); +} diff --git a/src/theme/Navbar/ColorModeToggle/styles.module.css b/src/theme/Navbar/ColorModeToggle/styles.module.css new file mode 100644 index 00000000..7bd077a6 --- /dev/null +++ b/src/theme/Navbar/ColorModeToggle/styles.module.css @@ -0,0 +1,3 @@ +.darkNavbarColorModeToggle:hover { + background: var(--ifm-color-gray-800); +} diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx new file mode 100644 index 00000000..f8bf21e1 --- /dev/null +++ b/src/theme/Navbar/Content/index.tsx @@ -0,0 +1,90 @@ +import React, {type ReactNode} from 'react'; +import {useThemeConfig, ErrorCauseBoundary} from '@docusaurus/theme-common'; +import { + splitNavbarItems, + useNavbarMobileSidebar, +} from '@docusaurus/theme-common/internal'; +import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import SearchBar from '@theme/SearchBar'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import NavbarLogo from '@theme/Navbar/Logo'; +import NavbarSearch from '@theme/Navbar/Search'; + +import styles from './styles.module.css'; + +function useNavbarItems() { + // TODO temporary casting until ThemeConfig type is improved + return useThemeConfig().navbar.items as NavbarItemConfig[]; +} + +function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element { + return ( + <> + {items.map((item, i) => ( + + new Error( + `A theme navbar item failed to render. +Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config: +${JSON.stringify(item, null, 2)}`, + {cause: error}, + ) + }> + + + ))} + + ); +} + +function NavbarContentLayout({ + left, + right, +}: { + left: ReactNode; + right: ReactNode; +}) { + return ( +
+
{left}
+
{right}
+
+ ); +} + +export default function NavbarContent(): JSX.Element { + const mobileSidebar = useNavbarMobileSidebar(); + + const items = useNavbarItems(); + const [leftItems, rightItems] = splitNavbarItems(items); + + const searchBarItem = items.find((item) => item.type === 'search'); + + return ( + + {!mobileSidebar.disabled && } + + + + } + right={ + // TODO stop hardcoding items? + // Ask the user to add the respective navbar items => more flexible + <> + + + {!searchBarItem && ( + + + + )} + + } + /> + ); +} diff --git a/src/theme/Navbar/Content/styles.module.css b/src/theme/Navbar/Content/styles.module.css new file mode 100644 index 00000000..4c9471e1 --- /dev/null +++ b/src/theme/Navbar/Content/styles.module.css @@ -0,0 +1,8 @@ +/* +Hide color mode toggle in small viewports + */ +@media (max-width: 996px) { + .colorModeToggle { + display: none; + } +} diff --git a/src/theme/Navbar/Layout/index.tsx b/src/theme/Navbar/Layout/index.tsx new file mode 100644 index 00000000..14abe9c3 --- /dev/null +++ b/src/theme/Navbar/Layout/index.tsx @@ -0,0 +1,51 @@ +import { translate } from '@docusaurus/Translate' +import { useThemeConfig } from '@docusaurus/theme-common' +import { useHideableNavbar, useNavbarMobileSidebar } from '@docusaurus/theme-common/internal' +import type { Props } from '@theme/Navbar/Layout' +import NavbarMobileSidebar from '@theme/Navbar/MobileSidebar' +import clsx from 'clsx' +import { type ComponentProps } from 'react' +import { useLocation } from 'react-router-dom' + +import styles from './styles.module.css' + +function NavbarBackdrop(props: ComponentProps<'div'>) { + return
+} + +export default function NavbarLayout({ children }: Props): JSX.Element { + const { + navbar: { hideOnScroll, style }, + } = useThemeConfig() + const mobileSidebar = useNavbarMobileSidebar() + const { navbarRef, isNavbarVisible } = useHideableNavbar(hideOnScroll) + + const location = useLocation() + const isHomePage = location.pathname === '/' + + return ( + + ) +} diff --git a/src/theme/Navbar/Layout/styles.module.css b/src/theme/Navbar/Layout/styles.module.css new file mode 100644 index 00000000..e72891a4 --- /dev/null +++ b/src/theme/Navbar/Layout/styles.module.css @@ -0,0 +1,7 @@ +.navbarHideable { + transition: transform var(--ifm-transition-fast) ease; +} + +.navbarHidden { + transform: translate3d(0, calc(-100% - 2px), 0); +} diff --git a/src/theme/Navbar/Logo/index.tsx b/src/theme/Navbar/Logo/index.tsx new file mode 100644 index 00000000..db71564b --- /dev/null +++ b/src/theme/Navbar/Logo/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Logo from '@theme/Logo'; + +export default function NavbarLogo(): JSX.Element { + return ( + + ); +} diff --git a/src/theme/Navbar/MobileSidebar/Header/index.tsx b/src/theme/Navbar/MobileSidebar/Header/index.tsx new file mode 100644 index 00000000..d1f1659b --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/Header/index.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import {translate} from '@docusaurus/Translate'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import IconClose from '@theme/Icon/Close'; +import NavbarLogo from '@theme/Navbar/Logo'; + +function CloseButton() { + const mobileSidebar = useNavbarMobileSidebar(); + return ( + + ); +} + +export default function NavbarMobileSidebarHeader(): JSX.Element { + return ( +
+ + + +
+ ); +} diff --git a/src/theme/Navbar/MobileSidebar/Layout/index.tsx b/src/theme/Navbar/MobileSidebar/Layout/index.tsx index dd1880ef..ac41cc39 100644 --- a/src/theme/Navbar/MobileSidebar/Layout/index.tsx +++ b/src/theme/Navbar/MobileSidebar/Layout/index.tsx @@ -2,7 +2,6 @@ import { useNavbarSecondaryMenu } from '@docusaurus/theme-common/internal' import UserCard from '@site/src/components/UserCard' import { cn } from '@site/src/lib/utils' import type { Props } from '@theme/Navbar/MobileSidebar/Layout' -import React from 'react' export default function NavbarMobileSidebarLayout({ header, primaryMenu, secondaryMenu }: Props): JSX.Element { const { shown: secondaryMenuShown } = useNavbarSecondaryMenu() diff --git a/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx new file mode 100644 index 00000000..db30be49 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem'; + +function useNavbarItems() { + // TODO temporary casting until ThemeConfig type is improved + return useThemeConfig().navbar.items as NavbarItemConfig[]; +} + +// The primary menu displays the navbar items +export default function NavbarMobilePrimaryMenu(): JSX.Element { + const mobileSidebar = useNavbarMobileSidebar(); + + // TODO how can the order be defined for mobile? + // Should we allow providing a different list of items? + const items = useNavbarItems(); + + return ( +
    + {items.map((item, i) => ( + mobileSidebar.toggle()} + key={i} + /> + ))} +
+ ); +} diff --git a/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx b/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx new file mode 100644 index 00000000..757f5967 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/SecondaryMenu/index.tsx @@ -0,0 +1,32 @@ +import React, {type ComponentProps} from 'react'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import {useNavbarSecondaryMenu} from '@docusaurus/theme-common/internal'; +import Translate from '@docusaurus/Translate'; + +function SecondaryMenuBackButton(props: ComponentProps<'button'>) { + return ( + + ); +} + +// 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/Toggle/index.tsx b/src/theme/Navbar/MobileSidebar/Toggle/index.tsx new file mode 100644 index 00000000..9691f103 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/Toggle/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import {translate} from '@docusaurus/Translate'; +import IconMenu from '@theme/Icon/Menu'; + +export default function MobileSidebarToggle(): JSX.Element { + const {toggle, shown} = useNavbarMobileSidebar(); + return ( + + ); +} diff --git a/src/theme/Navbar/MobileSidebar/index.tsx b/src/theme/Navbar/MobileSidebar/index.tsx new file mode 100644 index 00000000..484b319b --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { + useLockBodyScroll, + useNavbarMobileSidebar, +} from '@docusaurus/theme-common/internal'; +import NavbarMobileSidebarLayout from '@theme/Navbar/MobileSidebar/Layout'; +import NavbarMobileSidebarHeader from '@theme/Navbar/MobileSidebar/Header'; +import NavbarMobileSidebarPrimaryMenu from '@theme/Navbar/MobileSidebar/PrimaryMenu'; +import NavbarMobileSidebarSecondaryMenu from '@theme/Navbar/MobileSidebar/SecondaryMenu'; + +export default function NavbarMobileSidebar(): JSX.Element | null { + const mobileSidebar = useNavbarMobileSidebar(); + useLockBodyScroll(mobileSidebar.shown); + + if (!mobileSidebar.shouldRender) { + return null; + } + + return ( + } + primaryMenu={} + secondaryMenu={} + /> + ); +} diff --git a/src/theme/Navbar/Search/index.tsx b/src/theme/Navbar/Search/index.tsx new file mode 100644 index 00000000..788b3f1c --- /dev/null +++ b/src/theme/Navbar/Search/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import clsx from 'clsx'; +import type {Props} from '@theme/Navbar/Search'; + +import styles from './styles.module.css'; + +export default function NavbarSearch({ + children, + className, +}: Props): JSX.Element { + return ( +
+ {children} +
+ ); +} diff --git a/src/theme/Navbar/Search/styles.module.css b/src/theme/Navbar/Search/styles.module.css new file mode 100644 index 00000000..9eeb2934 --- /dev/null +++ b/src/theme/Navbar/Search/styles.module.css @@ -0,0 +1,21 @@ +/* +Workaround to avoid rendering empty search container +See https://github.com/facebook/docusaurus/pull/9385 +*/ +.navbarSearchContainer:empty { + display: none; +} + +@media (max-width: 996px) { + .navbarSearchContainer { + position: absolute; + right: var(--ifm-navbar-padding-horizontal); + } +} + +@media (min-width: 997px) { + .navbarSearchContainer { + padding: var(--ifm-navbar-item-padding-vertical) + var(--ifm-navbar-item-padding-horizontal); + } +} diff --git a/src/theme/Navbar/index.tsx b/src/theme/Navbar/index.tsx new file mode 100644 index 00000000..a9b61f1e --- /dev/null +++ b/src/theme/Navbar/index.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import NavbarLayout from '@theme/Navbar/Layout'; +import NavbarContent from '@theme/Navbar/Content'; + +export default function Navbar(): JSX.Element { + return ( + + + + ); +} diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx index 12bb6dbd..5735798f 100644 --- a/src/theme/TOC/index.tsx +++ b/src/theme/TOC/index.tsx @@ -3,7 +3,6 @@ import { cn } from '@site/src/lib/utils' import type { Props } from '@theme/TOC' import TOCItems from '@theme/TOCItems' import { motion } from 'framer-motion' -import React from 'react' import styles from './styles.module.css' const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight' @@ -15,16 +14,21 @@ export default function TOC({ className, ...props }: Props): JSX.Element { return ( - +
{`${readPercent}%`}
diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css index 3e674e09..c09ffd05 100644 --- a/src/theme/TOC/styles.module.css +++ b/src/theme/TOC/styles.module.css @@ -25,7 +25,6 @@ .percent { display: inline-block; margin: var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal); - padding-left: var(--ifm-toc-padding-horizontal); font-size: 0.9rem; color: var(--ifm-color-emphasis-600); }