diff --git a/packages/vkui-docs-theme/index.ts b/packages/vkui-docs-theme/index.ts index 52c46650194..933afae8660 100644 --- a/packages/vkui-docs-theme/index.ts +++ b/packages/vkui-docs-theme/index.ts @@ -1,2 +1,7 @@ import Layout from './src/index'; +import type { PartialDocsThemeConfig as DocsThemeConfig } from './src/types'; + +export { useFetch } from './src/hooks/useFetch'; +export { StorybookIcon, GithubIcon, FigmaIcon } from './src/icons'; +export { type DocsThemeConfig }; export default Layout; diff --git a/packages/vkui-docs-theme/src/components/Anchor.tsx b/packages/vkui-docs-theme/src/components/Anchor.tsx new file mode 100644 index 00000000000..77581dac34c --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Anchor.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { type LinkProps, Link as VKUILink } from '@vkontakte/vkui'; +import NextLink from 'next/link'; + +const EXTERNAL_HREF_REGEX = /https?:\/\//; + +const Link = React.forwardRef((props, ref) => ( + +)); +Link.displayName = 'VKUILinkWithRef'; + +export function Anchor({ href = '', children, ...props }: LinkProps) { + const newWindow = EXTERNAL_HREF_REGEX.test(href); + + if (newWindow) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.module.css b/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.module.css new file mode 100644 index 00000000000..2f919d9ad4e --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.module.css @@ -0,0 +1,25 @@ +.root { + display: flex; + align-items: center; + justify-content: flex-start; +} + +/* stylelint-disable-next-line selector-max-universal */ +.root > .item:not(:last-child), +.root > .icon { + margin-inline-end: 4px; +} + +.item { + color: var(--vkui--color_text_secondary); + + --vkui--color_text_link: var(--vkui--color_text_secondary); +} + +.icon { + color: var(--vkui--color_icon_secondary); +} + +.activeItem { + color: var(--vkui--color_text_primary); +} diff --git a/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.tsx b/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 00000000000..6fc07ae3eda --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,26 @@ +import { Fragment } from 'react'; +import { Icon12ChevronOutline } from '@vkontakte/icons'; +import { classNames, Footnote } from '@vkontakte/vkui'; +import type { Item } from 'nextra/normalize-pages'; +import { Anchor } from '../Anchor'; +import styles from './Breadcrumbs.module.css'; + +export function Breadcrumbs({ activePath }: { activePath: Item[] }) { + return ( +
+ {activePath.map((item, index) => { + const isLink = !item.children || item.withIndexPage; + const isActive = index === activePath.length - 1; + + return ( + + {index > 0 && } + + {isLink && !isActive ? {item.title} : item.title} + + + ); + })} +
+ ); +} diff --git a/packages/vkui-docs-theme/src/components/ColorSchemeSwitch.tsx b/packages/vkui-docs-theme/src/components/ColorSchemeSwitch.tsx new file mode 100644 index 00000000000..982a167c7c4 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/ColorSchemeSwitch.tsx @@ -0,0 +1,33 @@ +import { Icon20MoonOutline, Icon20SunOutline } from '@vkontakte/icons'; +import { AdaptivityProvider, SegmentedControl, Skeleton } from '@vkontakte/vkui'; +import { useMounted } from 'nextra/hooks'; +import { useColorScheme } from '../contexts'; + +const options = [ + { 'value': 'light', 'label': , 'aria-label': 'Переключить на светлую тему' }, + { 'value': 'dark', 'label': , 'aria-label': 'Переключить на тёмную тему' }, +]; + +const SWITCH_WIDTH = 94; + +export function ColorSchemeSwitch() { + const { setColorScheme, resolvedColorScheme } = useColorScheme(); + const mounted = useMounted(); + + if (!mounted) { + return ; + } + + return ( + + + + ); +} diff --git a/packages/vkui-docs-theme/src/components/Head.tsx b/packages/vkui-docs-theme/src/components/Head.tsx new file mode 100644 index 00000000000..c2ec36f0709 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Head.tsx @@ -0,0 +1,29 @@ +import NextHead from 'next/head'; +import { useMounted } from 'nextra/hooks'; +import { useColorScheme, useThemeConfig } from '../contexts'; + +export function Head() { + const themeConfig = useThemeConfig(); + const { resolvedColorScheme } = useColorScheme(); + const mounted = useMounted(); + + const head = typeof themeConfig.head === 'function' ? themeConfig.head({}) : themeConfig.head; + + return ( + + {mounted ? ( + + ) : ( + <> + + + + )} + + {head} + + ); +} diff --git a/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.module.css b/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.module.css new file mode 100644 index 00000000000..5a140d8f969 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.module.css @@ -0,0 +1,38 @@ +.root { + display: flex; + align-items: center; + padding-block-start: var(--vkui--spacing_size_4xl); + border-block-start: 1px solid var(--vkui_docs--color_stroke_separator_secondary); + margin-block-start: 56px; +} + +.navLinkItem { + display: flex; + flex-direction: column; + color: var(--vkui--color_text_subhead); + align-items: flex-end; + padding-block: var(--vkui--spacing_size_m); + padding-inline: var(--vkui--spacing_size_xl); + text-decoration: none; +} + +.navLink { + margin-block-start: var(--vkui--spacing_size_s); + display: flex; + align-items: center; + justify-content: flex-end; + color: var(--vkui--color_text_primary); +} + +.navLinkItemNext { + margin-inline-start: auto; + align-items: flex-start; +} + +.navLinkPrevIcon { + margin-inline-end: var(--vkui--spacing_size_m); +} + +.navLinkNextIcon { + margin-inline-start: var(--vkui--spacing_size_m); +} diff --git a/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.tsx b/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.tsx new file mode 100644 index 00000000000..2212cbc7358 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/NavLinks/NavLinks.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { Icon16ChevronLeft, Icon16ChevronOutline } from '@vkontakte/icons'; +import { classNames } from '@vkontakte/vkjs'; +import { Footnote, Headline, Tappable } from '@vkontakte/vkui'; +import NextLink from 'next/link'; +import type { Item } from 'nextra/normalize-pages'; +import { useThemeConfig } from '../../contexts'; +import styles from './NavLinks.module.css'; + +interface NavLinkProps { + currentIndex: number; + flatDirectories: Item[]; +} + +interface NavLinkItemProps { + route: string; + children: React.ReactNode; + className?: string; + type?: 'prev' | 'next'; +} + +function NavLinkItem({ type = 'prev', route, children, className }: NavLinkItemProps) { + return ( + + {type === 'next' ? 'Следующая' : 'Предыдущая'} +
{children}
+
+ ); +} + +export function NavLinks({ flatDirectories, currentIndex }: NavLinkProps) { + const themeConfig = useThemeConfig(); + const nav = themeConfig.navigation; + const navigation = typeof nav === 'boolean' ? { prev: nav, next: nav } : nav; + let prev = navigation.prev && flatDirectories[currentIndex - 1]; + let next = navigation.next && flatDirectories[currentIndex + 1]; + + if (prev && !prev.isUnderCurrentDocsTree) { + prev = false; + } + if (next && !next.isUnderCurrentDocsTree) { + next = false; + } + + if (!prev && !next) { + return null; + } + + return ( +
+ {prev && ( + + + {prev.title} + + )} + {next && ( + + {next.title} + + + )} +
+ ); +} diff --git a/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css b/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css new file mode 100644 index 00000000000..fecdac703c9 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css @@ -0,0 +1,102 @@ +.root { + position: sticky; + inset-block-start: 0; + z-index: 20; + inline-size: 100%; + background-color: transparent; + color: var(--vkui--color_text_primary); + border-block-end: var(--vkui--size_border--regular) solid + var(--vkui_docs--color_stroke_separator_secondary); + box-shadow: 0 8px 30px 0 rgba(0, 0, 0, 0.04); +} + +.navbar { + display: flex; + margin-inline: auto; + align-items: center; + block-size: var(--vkui_docs--navbar-height); + max-inline-size: var(--vkui_docs--max-width); + justify-content: space-between; + padding-block: var(--vkui--spacing_size_xl); + padding-inline: var(--vkui--spacing_size_2xl); + background-color: var(--vkui--color_background_content); +} + +@media (--viewWidth-desktopPlus) { + .navbar { + padding-inline: 40px; + padding-block: var(--vkui--spacing_size_4xl); + } +} + +.extraContent { + display: none; +} + +@media (--viewWidth-desktopPlus) { + .extraContent { + display: flex; + align-items: center; + } +} + +/* stylelint-disable-next-line selector-max-universal */ +.extraContent > *:not(:first-child) { + margin-inline-start: var(--vkui--spacing_size_m); +} + +.navbarLink { + color: var(--vkui--color_text_primary); + padding-block: 8px; + padding-inline: 16px; + text-decoration: none; +} + +.navbarLink:not(:last-child) { + margin-inline-end: var(--vkui--spacing_size_m); +} + +.navbarLinkActive { + background-color: var(--vkui--color_field_background); +} + +.links { + display: none; +} + +@media (--viewWidth-desktopPlus) { + .links { + display: flex; + block-size: 36px; + align-items: stretch; + } +} + +.separator.separator { + color: var(--vkui_docs--color_stroke_separator_secondary); + margin-inline-end: var(--vkui--spacing_size_m); +} + +.logoLink { + color: var(--vkui--color_text_primary); +} + +@media (--viewWidth-desktopPlus) { + .menuButton { + display: none; + } +} + +.menuIcon { + box-sizing: initial; +} + +.search { + inline-size: 200px; +} + +@media (--viewWidth-smallTabletMinus) { + .search { + display: none; + } +} diff --git a/packages/vkui-docs-theme/src/components/Navbar/Navbar.tsx b/packages/vkui-docs-theme/src/components/Navbar/Navbar.tsx new file mode 100644 index 00000000000..1caf51d7b2c --- /dev/null +++ b/packages/vkui-docs-theme/src/components/Navbar/Navbar.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { Icon24MenuOutline } from '@vkontakte/icons'; +import { classNames } from '@vkontakte/vkjs'; +import { + ButtonGroup, + Flex, + Headline, + IconButton, + Tappable, + type TappableProps, + Separator as VKUISeparator, +} from '@vkontakte/vkui'; +import NextLink from 'next/link'; +import { useFSRoute } from 'nextra/hooks'; +import type { PageItem } from 'nextra/normalize-pages'; +import { useMenu, useThemeConfig } from '../../contexts'; +import { renderComponent } from '../../helpers/render'; +import { type DocsThemeConfig } from '../../types'; +import { ColorSchemeSwitch } from '../ColorSchemeSwitch'; +import { ProjectButton } from '../ProjectButton'; +import styles from './Navbar.module.css'; + +export type NavbarProps = { + items: PageItem[]; +}; + +export function Navbar({ items }: NavbarProps): React.ReactElement { + const themeConfig = useThemeConfig(); + const activeRoute = useFSRoute(); + const { setMenu } = useMenu(); + + return ( +
+ +
+ ); +} + +function NavBarLink({ + title, + newWindow, + ...restProps +}: TappableProps & { title: PageItem['title']; newWindow?: boolean }) { + return ( + + {title} + + ); +} + +function Logo({ logo, logoLink }: Pick) { + return ( + + {renderComponent(logo)} + + ); +} + +function Separator() { + return ; +} diff --git a/packages/vkui-docs-theme/src/components/ProjectButton.tsx b/packages/vkui-docs-theme/src/components/ProjectButton.tsx new file mode 100644 index 00000000000..0d837ac5585 --- /dev/null +++ b/packages/vkui-docs-theme/src/components/ProjectButton.tsx @@ -0,0 +1,16 @@ +import { Button } from '@vkontakte/vkui'; +import { type DocsThemeConfig } from '../types'; + +export function ProjectButton({ icon, link }: Pick['project']) { + return ( +