diff --git a/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css b/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css new file mode 100644 index 0000000000000..50423d0327301 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.module.css @@ -0,0 +1,4 @@ +.icon { + @apply h-4 + w-4; +} diff --git a/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.tsx b/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.tsx new file mode 100644 index 0000000000000..1b106e45e9ee2 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbHomeLink/index.tsx @@ -0,0 +1,36 @@ +import HomeIcon from '@heroicons/react/24/outline/HomeIcon'; +import type { ComponentProps, FC } from 'react'; +import { useIntl } from 'react-intl'; + +import BreadcrumbLink from '@/components/Common/Breadcrumbs/BreadcrumbLink'; + +import styles from './index.module.css'; + +type BreadcrumbHomeLinkProps = Omit< + ComponentProps, + 'href' +> & + Partial, 'href'>>; + +const BreadcrumbHomeLink: FC = ({ + href = '/', + ...props +}) => { + const { formatMessage } = useIntl(); + + const navigateToHome = formatMessage({ + id: 'components.common.breadcrumbs.navigateToHome', + }); + + return ( + + + + ); +}; + +export default BreadcrumbHomeLink; diff --git a/components/Common/Breadcrumbs/BreadcrumbItem/index.module.css b/components/Common/Breadcrumbs/BreadcrumbItem/index.module.css new file mode 100644 index 0000000000000..e7cf634b2f5eb --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbItem/index.module.css @@ -0,0 +1,20 @@ +.item { + @apply flex + items-center + gap-5 + text-sm + font-medium + text-neutral-800 + dark:text-neutral-200; + + &.visuallyHidden { + @apply hidden; + } + + .separator { + @apply h-4 + w-4 + text-neutral-600 + dark:text-neutral-400; + } +} diff --git a/components/Common/Breadcrumbs/BreadcrumbItem/index.tsx b/components/Common/Breadcrumbs/BreadcrumbItem/index.tsx new file mode 100644 index 0000000000000..f78e3718af617 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbItem/index.tsx @@ -0,0 +1,42 @@ +import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon'; +import classNames from 'classnames'; +import type { ComponentProps, FC, PropsWithChildren } from 'react'; + +import styles from './index.module.css'; + +type BreadcrumbItemProps = { + disableMicrodata?: boolean; + hidden?: boolean; + hideSeparator?: boolean; + position?: number; +} & ComponentProps<'li'>; + +const BreadcrumbItem: FC> = ({ + disableMicrodata, + children, + hidden = false, + hideSeparator = false, + position, + ...props +}) => ( + +); + +export default BreadcrumbItem; diff --git a/components/Common/Breadcrumbs/BreadcrumbLink/index.module.css b/components/Common/Breadcrumbs/BreadcrumbLink/index.module.css new file mode 100644 index 0000000000000..5e0d74b85582c --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbLink/index.module.css @@ -0,0 +1,8 @@ +.active { + @apply rounded + bg-green-600 + px-2 + py-1 + font-semibold + text-white; +} diff --git a/components/Common/Breadcrumbs/BreadcrumbLink/index.tsx b/components/Common/Breadcrumbs/BreadcrumbLink/index.tsx new file mode 100644 index 0000000000000..b518aa7b50c2b --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbLink/index.tsx @@ -0,0 +1,31 @@ +import classNames from 'classnames'; +import type { ComponentProps, FC } from 'react'; + +import LocalizedLink from '@/components/LocalizedLink'; + +import styles from './index.module.css'; + +type BreadcrumbLinkProps = { + active?: boolean; +} & ComponentProps; + +const BreadcrumbLink: FC = ({ + href, + active, + ...props +}) => ( + + {props.children} + +); + +export default BreadcrumbLink; diff --git a/components/Common/Breadcrumbs/BreadcrumbRoot/index.module.css b/components/Common/Breadcrumbs/BreadcrumbRoot/index.module.css new file mode 100644 index 0000000000000..55bedbeb24394 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbRoot/index.module.css @@ -0,0 +1,5 @@ +.list { + @apply flex + items-center + gap-5; +} diff --git a/components/Common/Breadcrumbs/BreadcrumbRoot/index.tsx b/components/Common/Breadcrumbs/BreadcrumbRoot/index.tsx new file mode 100644 index 0000000000000..3e7dab5df8c90 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbRoot/index.tsx @@ -0,0 +1,20 @@ +import type { FC, PropsWithChildren, ComponentProps } from 'react'; + +import styles from './index.module.css'; + +const BreadcrumbRoot: FC>> = ({ + children, + ...props +}) => ( + +); + +export default BreadcrumbRoot; diff --git a/components/Common/Breadcrumbs/BreadcrumbTruncatedItem/index.tsx b/components/Common/Breadcrumbs/BreadcrumbTruncatedItem/index.tsx new file mode 100644 index 0000000000000..557baee9c1085 --- /dev/null +++ b/components/Common/Breadcrumbs/BreadcrumbTruncatedItem/index.tsx @@ -0,0 +1,9 @@ +import BreadcrumbItem from '@/components/Common/Breadcrumbs/BreadcrumbItem'; + +const BreadcrumbTruncatedItem = () => ( + + + +); + +export default BreadcrumbTruncatedItem; diff --git a/components/Common/Breadcrumbs/index.stories.tsx b/components/Common/Breadcrumbs/index.stories.tsx new file mode 100644 index 0000000000000..1e25483c2ee71 --- /dev/null +++ b/components/Common/Breadcrumbs/index.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import Breadcrumbs from './'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + links: [ + { + label: 'Learn', + href: '/learn', + }, + { + label: 'Getting Started', + href: '/learn/getting-started', + }, + { + label: 'Introduction to Node.js', + href: '/learn/getting-started/intro', + }, + ], + }, +}; + +export const Truncate: Story = { + args: { + links: [ + { + label: 'Learn', + href: '/learn', + }, + { + label: 'Getting Started', + href: '/learn/getting-started', + }, + { + label: 'Introduction to Node.js', + href: '/learn/getting-started/intro', + }, + { + label: 'Installation', + href: '/learn/getting-started/intro/installation', + }, + { + label: 'Documentation', + href: '/learn/getting-started/intro/installation/documentation', + }, + ], + maxLength: 1, + }, +}; + +export const HiddenHome: Story = { + args: { + hideHome: true, + links: [ + { + label: 'Learn', + href: '/learn', + }, + { + label: 'Getting Started', + href: '/learn/getting-started', + }, + { + label: 'Introduction to Node.js', + href: '/learn/getting-started/intro', + }, + ], + }, +}; + +export default { component: Breadcrumbs } as Meta; diff --git a/components/Common/Breadcrumbs/index.tsx b/components/Common/Breadcrumbs/index.tsx new file mode 100644 index 0000000000000..bafede32626e4 --- /dev/null +++ b/components/Common/Breadcrumbs/index.tsx @@ -0,0 +1,68 @@ +import { type LinkProps } from 'next/link'; +import { useMemo, type FC } from 'react'; + +import BreadcrumbHomeLink from './BreadcrumbHomeLink'; +import BreadcrumbItem from './BreadcrumbItem'; +import BreadcrumbLink from './BreadcrumbLink'; +import BreadcrumbRoot from './BreadcrumbRoot'; +import BreadcrumbTruncatedItem from './BreadcrumbTruncatedItem'; + +type BreadcrumbLink = { + label: string; + href: LinkProps['href']; +}; + +type BreadcrumbsProps = { + links: BreadcrumbLink[]; + maxLength?: number; + hideHome?: boolean; +}; + +const Breadcrumbs: FC = ({ + links = [], + maxLength = 5, + hideHome = false, +}) => { + const totalLength = links.length + +!hideHome; + const lengthOffset = maxLength - totalLength; + const isOverflow = lengthOffset < 0; + + const items = useMemo( + () => + links.map((link, index, items) => { + const position = index + 1; + const isLastItem = index === items.length - 1; + const hidden = + // We add 1 here to take into account of the truncated breadcrumb. + position <= Math.abs(lengthOffset) + 1 && isOverflow && !isLastItem; + + return ( + + ); + }), + [hideHome, isOverflow, lengthOffset, links] + ); + + return ( + + {!hideHome && ( + + + + )} + {isOverflow && } + {items} + + ); +}; + +export default Breadcrumbs; diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 0490dbd43f5ef..4c1f2e4be56c7 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -34,6 +34,7 @@ "components.header.buttons.toggleDarkMode": "Toggle dark/light mode", "components.pagination.next": "Newer | ", "components.pagination.previous": "Older", + "components.common.breadcrumbs.navigateToHome": "Navigate to Home", "components.common.crossLink.previous": "Prev", "components.common.crossLink.next": "Next", "components.common.codebox.copied": "Copied to clipboard!",