diff --git a/src/components/ArticleNavigation/ArticleNavigation.scss b/src/components/ArticleNavigation/ArticleNavigation.scss
new file mode 100644
index 000000000000..3b885e3b9178
--- /dev/null
+++ b/src/components/ArticleNavigation/ArticleNavigation.scss
@@ -0,0 +1,66 @@
+@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables;
+@use '../../variables.scss';
+
+$block: '.#{variables.$ns}article-navigation';
+
+#{$block} {
+ display: flex;
+ gap: var(--g-spacing-6);
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) {
+ gap: var(--g-spacing-5);
+ }
+ &__content {
+ padding: var(--g-spacing-2) 0;
+ overflow: hidden;
+ width: 100%;
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) {
+ padding: var(--g-spacing-3) 0;
+ }
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm') - 1) {
+ display: none;
+ }
+ &-title {
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) {
+ display: none;
+ }
+ }
+ }
+ &__button {
+ cursor: pointer;
+ text-decoration: none;
+ display: flex;
+ gap: var(--g-spacing-3);
+ width: 50%;
+ border-radius: 16px;
+ border: 1px solid var(--g-color-sfx-fade);
+ padding: var(--g-spacing-1);
+ justify-content: space-between;
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm') - 1) {
+ width: unset;
+ }
+ &_reverse {
+ flex-direction: row-reverse;
+ margin-left: auto;
+ padding-left: var(--g-spacing-5);
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm') - 1) {
+ padding-left: var(--g-spacing-1);
+ }
+ }
+ &-icon {
+ flex: none;
+ border-radius: 12px;
+ background: var(--g-color-base-generic);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--g-color-text-primary);
+ padding: 0 var(--g-spacing-5);
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) {
+ padding: 0 var(--g-spacing-3);
+ }
+ @media (max-width: map-get(pcVariables.$gridBreakpoints, 'sm') - 1) {
+ padding: var(--g-spacing-3);
+ }
+ }
+ }
+}
diff --git a/src/components/ArticleNavigation/ArticleNavigation.tsx b/src/components/ArticleNavigation/ArticleNavigation.tsx
new file mode 100644
index 000000000000..cf05cf033c78
--- /dev/null
+++ b/src/components/ArticleNavigation/ArticleNavigation.tsx
@@ -0,0 +1,82 @@
+import {ArrowLeft, ArrowRight} from '@gravity-ui/icons';
+import {Flex, Icon, Text} from '@gravity-ui/uikit';
+import Link from 'next/link';
+import {useCallback} from 'react';
+import {CONTENT_WRAPPER_ID} from 'src/constants';
+import {block} from 'src/utils';
+
+import {SubSection} from '../NavigationLayout/types';
+
+const b = block('article-navigation');
+
+import './ArticleNavigation.scss';
+
+interface ArticleNavigationProps {
+ prevSection: SubSection | null;
+ nextSection: SubSection | null;
+}
+
+export const ArticleNavigation = ({prevSection, nextSection}: ArticleNavigationProps) => {
+ const scrollTop = useCallback(() => {
+ const content = document.getElementById(CONTENT_WRAPPER_ID);
+ if (content) {
+ content.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }
+ }, []);
+
+ const linkClickHandler = () => {
+ scrollTop();
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/ArticleNavigation/index.ts b/src/components/ArticleNavigation/index.ts
new file mode 100644
index 000000000000..b6d4f0b45b01
--- /dev/null
+++ b/src/components/ArticleNavigation/index.ts
@@ -0,0 +1 @@
+export {ArticleNavigation} from './ArticleNavigation';
diff --git a/src/components/Component/Component.scss b/src/components/Component/Component.scss
index 4f2ff9ff39fb..ac97c8dad0a6 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: var(--g-spacing-10);
+ }
+
&__header {
display: flex;
align-items: center;
diff --git a/src/components/Component/Component.tsx b/src/components/Component/Component.tsx
index a64b04a9cbc3..fb226ccddc5f 100644
--- a/src/components/Component/Component.tsx
+++ b/src/components/Component/Component.tsx
@@ -8,6 +8,8 @@ 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 {ArticleNavigation} from '../ArticleNavigation/ArticleNavigation';
+import {Section} from '../NavigationLayout/types';
import {SandboxBlock} from '../SandboxBlock';
import './Component.scss';
@@ -34,15 +36,64 @@ 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;
const [activeTab, setActiveTab] = React.useState(
tabId === Tab.Design ? Tab.Design : Tab.Overview,
);
+
+ const currentSection = React.useMemo(
+ () => sections.find((item) => item.id === libId),
+ [libId, sections],
+ );
+
+ const currentIndex = React.useMemo(() => {
+ if (!currentSection || !currentSection.subSections) {
+ return null;
+ }
+ return currentSection.subSections.findIndex((item) => item.id === component.id);
+ }, [currentSection, component.id]);
+
+ const nextSection = React.useMemo(() => {
+ if (
+ !currentSection ||
+ !currentSection.subSections ||
+ (!currentIndex && currentIndex !== 0)
+ ) {
+ return null;
+ }
+ const nextIndex = currentIndex + 1;
+ if (nextIndex >= currentSection.subSections.length) {
+ return null;
+ }
+ return currentSection.subSections[nextIndex];
+ }, [currentIndex, currentSection]);
+
+ const prevSection = React.useMemo(() => {
+ if (
+ !currentSection ||
+ !currentSection.subSections ||
+ (!currentIndex && currentIndex !== 0)
+ ) {
+ return null;
+ }
+ const prevIndex = currentIndex - 1;
+ if (prevIndex < 0) {
+ return null;
+ }
+ return currentSection.subSections[prevIndex];
+ }, [currentIndex, currentSection]);
+
React.useEffect(() => {
setActiveTab(tabId === Tab.Design ? Tab.Design : Tab.Overview);
}, [tabId]);
@@ -140,6 +191,12 @@ export const Component: React.FC = ({libId, component, readmeCon
rewriteLinks={rewriteLinks}
withComponents
/>
+
>
)}
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 (
= ({article}) => {
+export const DesignArticle: React.FC = ({article, sectionId, sections}) => {
+ const currentSection = useMemo(
+ () => sections.find((item) => item.id === sectionId),
+ [sectionId, sections],
+ );
+
+ const currentIndex = useMemo(() => {
+ if (!currentSection || !currentSection.subSections) {
+ return null;
+ }
+ return currentSection.subSections.findIndex((item) => item.id === article.id);
+ }, [currentSection, article.id]);
+
+ const nextSection = useMemo(() => {
+ if (
+ !currentSection ||
+ !currentSection.subSections ||
+ (!currentIndex && currentIndex !== 0)
+ ) {
+ return null;
+ }
+ const nextIndex = currentIndex + 1;
+ if (nextIndex >= currentSection.subSections.length) {
+ return null;
+ }
+ return currentSection.subSections[nextIndex];
+ }, [currentIndex, currentSection]);
+
+ const prevSection = useMemo(() => {
+ if (
+ !currentSection ||
+ !currentSection.subSections ||
+ (!currentIndex && currentIndex !== 0)
+ ) {
+ return null;
+ }
+ const prevIndex = currentIndex - 1;
+ if (prevIndex < 0) {
+ return null;
+ }
+ return currentSection.subSections[prevIndex];
+ }, [currentIndex, currentSection]);
+
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(({id, title, components}) => ({
+ id: id,
+ title: title,
+ // url: `/components/${lib.id}`, // "Overview" link
+ subSections: components.map((componentItem) => ({
+ id: componentItem.id,
+ title: componentItem.title,
+ url:
+ componentItem.isComingSoon === true
+ ? '#'
+ : `/components/${lib.id}/${componentItem.id}`,
+ isComingSoon: componentItem.isComingSoon,
+ })),
+ }));
+ }, []);
+
return (
-
-
+
+
);
diff --git a/src/pages/design/[sectionId]/[articleId].tsx b/src/pages/design/[sectionId]/[articleId].tsx
index 2eaf2443d660..d1843377f84a 100644
--- a/src/pages/design/[sectionId]/[articleId].tsx
+++ b/src/pages/design/[sectionId]/[articleId].tsx
@@ -1,13 +1,15 @@
import {GetStaticPaths, GetStaticProps} from 'next';
+import {useMemo} from 'react';
+import {Section} from 'src/components/NavigationLayout/types';
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 +29,32 @@ export const getStaticProps: GetStaticProps = async (context) => {
};
export const ArticlePage = ({sectionId, articleId}: {sectionId: string; articleId: string}) => {
- const section = sections.find((item) => item.id === sectionId);
+ 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(({id, title, articles}) => ({
+ id: id,
+ title: title,
+ // Uncomment it to show overview tab
+ // url: `/design/${section.id}`,
+ subSections: articles.map((articleItem) => ({
+ id: articleItem.id,
+ title: articleItem.title,
+ url: `/design/${id}/${articleItem.id}`,
+ })),
+ }));
+ return result;
+ }, []);
+
return (
-
-
+
+
);