diff --git a/src/components/ArticleNavigations/ArticleNavigations.scss b/src/components/ArticleNavigations/ArticleNavigations.scss new file mode 100644 index 000000000000..01966bc8b849 --- /dev/null +++ b/src/components/ArticleNavigations/ArticleNavigations.scss @@ -0,0 +1,28 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '../../variables.scss'; + +$block: '.#{variables.$ns}article-navigations'; + +#{$block} { + display: flex; + gap: 24px; + &__item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px; + &_full { + justify-content: space-between; + padding-left: 20px; + } + &-button { + width: 56px; + height: 100%; + border-radius: 11px; + overflow: hidden; + } + } +} diff --git a/src/components/ArticleNavigations/ArticleNavigations.tsx b/src/components/ArticleNavigations/ArticleNavigations.tsx new file mode 100644 index 000000000000..0846113aae1c --- /dev/null +++ b/src/components/ArticleNavigations/ArticleNavigations.tsx @@ -0,0 +1,52 @@ +import {ArrowLeft, ArrowRight} from '@gravity-ui/icons'; +import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; +import {block} from 'src/utils'; + +const b = block('article-navigations'); + +import './ArticleNavigations.scss'; + +interface ArticleNavigationsProps { + previousTitle: string; + nextTitle: string; + nextHandler: () => void; + prevHandler: () => void; +} + +export const ArticleNavigations = ({ + previousTitle, + nextTitle, + nextHandler, + prevHandler, +}: ArticleNavigationsProps) => { + return ( +
+
+ + + + Previous + + + {previousTitle} + + +
+
+ + + Next + + + {nextTitle} + + + +
+
+ ); +}; diff --git a/src/components/ArticleNavigations/index.ts b/src/components/ArticleNavigations/index.ts new file mode 100644 index 000000000000..f456dad0ba45 --- /dev/null +++ b/src/components/ArticleNavigations/index.ts @@ -0,0 +1 @@ +export {ArticleNavigations} from './ArticleNavigations'; diff --git a/src/components/Component/Component.scss b/src/components/Component/Component.scss index 4f2ff9ff39fb..ddd97ca397d7 100644 --- a/src/components/Component/Component.scss +++ b/src/components/Component/Component.scss @@ -19,6 +19,10 @@ $block: '.#{variables.$ns}component'; border-radius: 10px; } + &__navigation { + margin-top: 40px; + } + &__header { display: flex; align-items: center; diff --git a/src/components/Component/Component.tsx b/src/components/Component/Component.tsx index a64b04a9cbc3..5b66a0b4bd15 100644 --- a/src/components/Component/Component.tsx +++ b/src/components/Component/Component.tsx @@ -1,13 +1,16 @@ import {Button, Icon, Tabs} from '@gravity-ui/uikit'; import {URL} from 'next/dist/compiled/@edge-runtime/primitives/url'; import {useRouter} from 'next/router'; -import React from 'react'; +import React, {useMemo} from 'react'; +import {CONTENT_WRAPPER_ID} from 'src/constants'; import figmaIcon from '../../assets/icons/figma.svg'; import githubIcon from '../../assets/icons/github.svg'; import {MDXRenderer} from '../../components/MDXRenderer/MDXRenderer'; import {Component as ComponentType} from '../../content/components/types'; import {block, getRouteFromReadmeUrl} from '../../utils'; +import {ArticleNavigations} from '../ArticleNavigations'; +import {Section} from '../NavigationLayout/types'; import {SandboxBlock} from '../SandboxBlock'; import './Component.scss'; @@ -34,9 +37,15 @@ export type ComponentProps = { libId: string; component: ComponentType; readmeContent: string; + sections: Section[]; }; -export const Component: React.FC = ({libId, component, readmeContent}) => { +export const Component: React.FC = ({ + libId, + component, + readmeContent, + sections, +}) => { const router = useRouter(); const {tabId} = router.query; @@ -66,6 +75,56 @@ export const Component: React.FC = ({libId, component, readmeCon [component.content?.readmeUrl], ); + const currentSection = useMemo( + () => sections.find((item) => item.id === libId), + [libId, sections], + ); + + const currentIndex = useMemo(() => { + if (!currentSection) { + return null; + } + return currentSection.subSections.findIndex((item) => item.id === component.id); + }, [currentSection, component.id]); + + const nextSection = useMemo(() => { + if (!currentSection) { + return null; + } + const nextIndex = (currentIndex + 1) % currentSection.subSections.length; + return currentSection.subSections[nextIndex]; + }, [currentIndex, currentSection]); + + const prevSection = useMemo(() => { + if (!currentSection) { + return null; + } + const prevIndex = + (currentIndex - 1 + currentSection.subSections.length) % + currentSection.subSections.length; + return currentSection.subSections[prevIndex]; + }, [currentIndex, currentSection]); + + const scrollTop = React.useCallback(() => { + const content = document.getElementById(CONTENT_WRAPPER_ID); + if (content) { + content.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + }, []); + + const onNextHandler = () => { + router.push(nextSection.url); + scrollTop(); + }; + + const onPrevHandler = () => { + router.push(prevSection.url); + scrollTop(); + }; + return (
@@ -140,6 +199,16 @@ export const Component: React.FC = ({libId, component, readmeCon rewriteLinks={rewriteLinks} withComponents /> + {typeof window !== 'undefined' ? ( +
+ +
+ ) : null} )}
diff --git a/src/components/ComponentsLayout/ComponentsLayout.tsx b/src/components/ComponentsLayout/ComponentsLayout.tsx index 28581de46fce..61803288dfae 100644 --- a/src/components/ComponentsLayout/ComponentsLayout.tsx +++ b/src/components/ComponentsLayout/ComponentsLayout.tsx @@ -1,34 +1,20 @@ -import React, {useMemo} from 'react'; +import React from 'react'; -import {libs} from '../../content/components'; import {NavigationLayout, Section} from '../NavigationLayout/NavigationLayout'; export type ComponentsLayoutProps = { libId: string; componentId?: string; children?: React.ReactNode; + sections: Section[]; }; export const ComponentsLayout: React.FC = ({ libId, componentId, children, + sections, }) => { - const sections = useMemo(() => { - return libs.map((lib) => ({ - id: lib.id, - title: lib.title, - // url: `/components/${lib.id}`, // "Overview" link - subSections: lib.components.map((component) => ({ - id: component.id, - title: component.title, - url: - component.isComingSoon === true ? '#' : `/components/${lib.id}/${component.id}`, - isComingSoon: component.isComingSoon, - })), - })); - }, []); - return ( void; + prevHandler: () => void; }; -export const DesignArticle: React.FC = ({article}) => { +export const DesignArticle: React.FC = ({ + article, + previousTitle, + nextTitle, + nextHandler, + prevHandler, +}) => { return (

{article.title}

+
); }; diff --git a/src/components/DesignLayout/DesignLayout.tsx b/src/components/DesignLayout/DesignLayout.tsx index 84b2344eab73..37f0e214ece6 100644 --- a/src/components/DesignLayout/DesignLayout.tsx +++ b/src/components/DesignLayout/DesignLayout.tsx @@ -1,30 +1,20 @@ -import React, {useMemo} from 'react'; +import React from 'react'; -import {sections as designSections} from '../../content/design'; import {NavigationLayout, Section} from '../NavigationLayout/NavigationLayout'; export type DesignLayoutProps = { sectionId: string; articleId?: string; children?: React.ReactNode; + sections: Section[]; }; -export const DesignLayout: React.FC = ({sectionId, articleId, children}) => { - const sections = useMemo(() => { - const result: Section[] = designSections.map((section) => ({ - id: section.id, - title: section.title, - // Uncomment it to show overview tab - // url: `/design/${section.id}`, - subSections: section.articles.map((article) => ({ - id: article.id, - title: article.title, - url: `/design/${section.id}/${article.id}`, - })), - })); - return result; - }, []); - +export const DesignLayout: React.FC = ({ + sectionId, + articleId, + children, + sections, +}) => { return ( (() => { + return libs.map((lib) => ({ + id: lib.id, + title: lib.title, + // url: `/components/${lib.id}`, // "Overview" link + subSections: lib.components.map((component) => ({ + id: component.id, + title: component.title, + url: + component.isComingSoon === true ? '#' : `/components/${lib.id}/${component.id}`, + isComingSoon: component.isComingSoon, + })), + })); + }, []); + return ( - - + + ); diff --git a/src/pages/design/[sectionId]/[articleId].tsx b/src/pages/design/[sectionId]/[articleId].tsx index 2eaf2443d660..0bc26cbc4d7e 100644 --- a/src/pages/design/[sectionId]/[articleId].tsx +++ b/src/pages/design/[sectionId]/[articleId].tsx @@ -1,13 +1,17 @@ import {GetStaticPaths, GetStaticProps} from 'next'; +import {useRouter} from 'next/router'; +import {useCallback, useMemo} from 'react'; +import {Section} from 'src/components/NavigationLayout/types'; +import {CONTENT_WRAPPER_ID} from 'src/constants'; import {DesignArticle} from '../../../components/DesignArticle/DesignArticle'; import {DesignLayout} from '../../../components/DesignLayout/DesignLayout'; import {Layout} from '../../../components/Layout/Layout'; -import {sections} from '../../../content/design'; +import {sections as designSections} from '../../../content/design'; export const getStaticPaths: GetStaticPaths = async () => { return { - paths: sections.reduce<{params: {sectionId: string; articleId: string}}[]>( + paths: designSections.reduce<{params: {sectionId: string; articleId: string}}[]>( (acc, section) => { section.articles.forEach((article) => { acc.push({params: {sectionId: section.id, articleId: article.id}}); @@ -27,17 +31,90 @@ export const getStaticProps: GetStaticProps = async (context) => { }; export const ArticlePage = ({sectionId, articleId}: {sectionId: string; articleId: string}) => { - const section = sections.find((item) => item.id === sectionId); + const router = useRouter(); + const section = designSections.find((item) => item.id === sectionId); const article = section?.articles.find((item) => item.id === articleId); if (!section || !article) { return null; } + const sections = useMemo(() => { + const result: Section[] = designSections.map((section) => ({ + id: section.id, + title: section.title, + // Uncomment it to show overview tab + // url: `/design/${section.id}`, + subSections: section.articles.map((article) => ({ + id: article.id, + title: article.title, + url: `/design/${section.id}/${article.id}`, + })), + })); + return result; + }, []); + + const currentSection = useMemo( + () => sections.find((item) => item.id === sectionId), + [sectionId, sections], + ); + + const currentIndex = useMemo(() => { + if (!currentSection) { + return null; + } + return currentSection.subSections.findIndex((item) => item.id === articleId); + }, [currentSection, articleId]); + + const nextSection = useMemo(() => { + if (!currentSection) { + return null; + } + const nextIndex = (currentIndex + 1) % currentSection.subSections.length; + return currentSection.subSections[nextIndex]; + }, [currentIndex, currentSection]); + + const prevSection = useMemo(() => { + if (!currentSection) { + return null; + } + const prevIndex = + (currentIndex - 1 + currentSection.subSections.length) % + currentSection.subSections.length; + return currentSection.subSections[prevIndex]; + }, [currentIndex, currentSection]); + + const scrollTop = useCallback(() => { + const content = document.getElementById(CONTENT_WRAPPER_ID); + if (content) { + content.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + }, []); + + const onNextHandler = () => { + router.push(nextSection.url); + scrollTop(); + }; + + const onPrevHandler = () => { + router.push(prevSection.url); + scrollTop(); + }; + return ( - - + + );