Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/site/components/Common/ArticleWithSidebarState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import Article from '@node-core/ui-components/Containers/Article';

import { useSidebarState } from '#site/providers/sidebarStateProvider';

import type { FC, PropsWithChildren } from 'react';

const ArticleWithSidebarState: FC<PropsWithChildren> = ({ children }) => {
const { isLeftSidebarCollapsed, isRightSidebarCollapsed } = useSidebarState();

return (
<Article
data-left-sidebar-collapsed={isLeftSidebarCollapsed ? 'true' : 'false'}
data-right-sidebar-collapsed={isRightSidebarCollapsed ? 'true' : 'false'}
>
{children}
</Article>
);
};

export default ArticleWithSidebarState;
26 changes: 26 additions & 0 deletions apps/site/components/Common/CollapsedSidebarRail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import type { FC, PropsWithChildren } from 'react';

type CollapsedSidebarRailProps = {
side: 'left' | 'right';
};

const CollapsedSidebarRail: FC<
PropsWithChildren<CollapsedSidebarRailProps>
> = ({ side, children }) => {
return (
<aside
className={`flex w-full flex-col bg-white transition-all duration-200 ease-out dark:bg-neutral-950 ${

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use classnames + css module + apply directive

side === 'left'
? 'border-r border-neutral-200 dark:border-neutral-900'
: 'border-l border-neutral-200 dark:border-neutral-900'
}`}
>
{/* Button container positioned at top with proper spacing */}
<div className="flex w-full justify-center px-3 pt-6">{children}</div>
</aside>
);
};

export default CollapsedSidebarRail;
27 changes: 27 additions & 0 deletions apps/site/components/Common/ContentLayoutWithSidebarState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useSidebarState } from '#site/providers/sidebarStateProvider';

import type { FC, PropsWithChildren } from 'react';

type ContentLayoutWithSidebarStateProps = {
className?: string;
};

const ContentLayoutWithSidebarState: FC<
PropsWithChildren<ContentLayoutWithSidebarStateProps>
> = ({ children, className = '' }) => {
const { isLeftSidebarCollapsed, isRightSidebarCollapsed } = useSidebarState();

return (
<div
className={className}
data-left-sidebar-collapsed={isLeftSidebarCollapsed ? 'true' : 'false'}
data-right-sidebar-collapsed={isRightSidebarCollapsed ? 'true' : 'false'}
>
{children}
</div>
);
};

export default ContentLayoutWithSidebarState;
29 changes: 29 additions & 0 deletions apps/site/components/Common/SidebarToggleButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@reference "../../styles/index.css";

.toggleButton {
@apply flex
size-8
items-center
justify-center
rounded-md
border
border-neutral-200
bg-white
text-neutral-700
shadow-sm
transition-all
duration-200
ease-out
hover:bg-neutral-50
hover:shadow-md
focus:ring-2
focus:ring-neutral-400
focus:ring-offset-1
focus:outline-none
dark:border-neutral-700
dark:bg-neutral-800
dark:text-neutral-300
dark:shadow-neutral-900/20
dark:hover:bg-neutral-700
dark:focus:ring-neutral-500;
}
61 changes: 61 additions & 0 deletions apps/site/components/Common/SidebarToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { useTranslations } from 'next-intl';

import type { FC, ButtonHTMLAttributes } from 'react';

import styles from './SidebarToggleButton.module.css';

type SidebarToggleButtonProps = {
side: 'left' | 'right';
isCollapsed: boolean;
onToggle: () => void;
} & ButtonHTMLAttributes<HTMLButtonElement>;

const SidebarToggleButton: FC<SidebarToggleButtonProps> = ({
side,
isCollapsed,
onToggle,
...props
}) => {
const t = useTranslations();

const icon =
side === 'left' ? (
isCollapsed ? (
<ChevronRightIcon className="size-4" />
) : (
<ChevronLeftIcon className="size-4" />
)
) : isCollapsed ? (
<ChevronLeftIcon className="size-4" />
) : (
<ChevronRightIcon className="size-4" />
);
Comment on lines +24 to +35

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const icon =
side === 'left' ? (
isCollapsed ? (
<ChevronRightIcon className="size-4" />
) : (
<ChevronLeftIcon className="size-4" />
)
) : isCollapsed ? (
<ChevronLeftIcon className="size-4" />
) : (
<ChevronRightIcon className="size-4" />
);
const icon =
side === 'left' ? isCollapsed : !isCollapsed ? (
<ChevronRightIcon className="size-4" />
) : (
<ChevronLeftIcon className="size-4" />
);


const label =
side === 'left'
? isCollapsed
? t('components.common.sidebar.expandLeftSidebar')
: t('components.common.sidebar.collapseLeftSidebar')
: isCollapsed
? t('components.common.sidebar.expandRightSidebar')
: t('components.common.sidebar.collapseRightSidebar');
Comment on lines +22 to +44

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const t = useTranslations();
const icon =
side === 'left' ? (
isCollapsed ? (
<ChevronRightIcon className="size-4" />
) : (
<ChevronLeftIcon className="size-4" />
)
) : isCollapsed ? (
<ChevronLeftIcon className="size-4" />
) : (
<ChevronRightIcon className="size-4" />
);
const label =
side === 'left'
? isCollapsed
? t('components.common.sidebar.expandLeftSidebar')
: t('components.common.sidebar.collapseLeftSidebar')
: isCollapsed
? t('components.common.sidebar.expandRightSidebar')
: t('components.common.sidebar.collapseRightSidebar');
const t = useTranslations();
const direction = side === 'left' ? 'Left' : 'Right';
const action = isCollapsed ? 'expand' : 'collapse';
const icon =
side === 'left' === isCollapsed ? (
<ChevronRightIcon className="size-4" />
) : (
<ChevronLeftIcon className="size-4" />
);
const label = t(
`components.common.sidebar.${action}${direction}Sidebar`
);


return (
<button
type="button"
onClick={onToggle}
aria-label={label}
aria-expanded={!isCollapsed}
title={label}
className={styles.toggleButton}
{...props}
>
{icon}
</button>
);
};

export default SidebarToggleButton;
45 changes: 43 additions & 2 deletions apps/site/components/withMetaBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import GitHubIcon from '@node-core/ui-components/Icons/Social/GitHub';
import { defaultLocale } from '@node-core/website-i18n';
import { useFormatter, useLocale, useTranslations } from 'next-intl';

import CollapsedSidebarRail from '#site/components/Common/CollapsedSidebarRail';
import SidebarToggleButton from '#site/components/Common/SidebarToggleButton';
import Link from '#site/components/Link';
import WithAvatarGroup from '#site/components/withAvatarGroup';
import useClientContext from '#site/hooks/useClientContext';
import useMediaQuery from '#site/hooks/useMediaQuery';
import { DEFAULT_DATE_FORMAT } from '#site/next.calendar.constants.mjs';
import { TRANSLATION_URL } from '#site/next.constants.mjs';
import { useSidebarState } from '#site/providers/sidebarStateProvider';
import { getGitHubBlobUrl } from '#site/util/github';

import type { FC } from 'react';

const WithMetaBar: FC = () => {
const WithMetaBar: FC<{ disableToggle?: boolean }> = ({
disableToggle = false,
}) => {
const { headings, readingTime, frontmatter, filename } = useClientContext();
const formatter = useFormatter();
const lastUpdated = frontmatter.date
Expand All @@ -34,12 +39,38 @@ const WithMetaBar: FC = () => {

const t = useTranslations();
const locale = useLocale();
const { isRightSidebarCollapsed, toggleRightSidebar } = useSidebarState();

// Since we cannot show the same number of avatars in Mobile / Tablet
// resolution as we do on desktop and there is overflow, we are adjusting
// the number of avatars manually for the resolutions below
const isSmallerThanDesktop = useMediaQuery('(max-width: 1280px)');

// Check if there's any content to show in the metabar
const hasContent =
lastUpdated ||
readingTimeText ||
usernames.length > 0 ||
headings.length > 0;

// If no content, render empty div to preserve grid structure
if (!hasContent) {
return <div />;
}

// Show collapsed rail when right sidebar is collapsed and toggle is enabled
if (isRightSidebarCollapsed && !disableToggle) {
return (
<CollapsedSidebarRail side="right">
<SidebarToggleButton
side="right"
isCollapsed={isRightSidebarCollapsed}
onToggle={toggleRightSidebar}
/>
</CollapsedSidebarRail>
);
}

return (
<MetaBar
heading={t('components.metabar.tableOfContents')}
Expand Down Expand Up @@ -74,7 +105,17 @@ const WithMetaBar: FC = () => {
),
}}
headings={{ items: headings }}
/>
>
{!disableToggle && (
<div className="mb-1 flex justify-end pr-2">
<SidebarToggleButton
side="right"
isCollapsed={isRightSidebarCollapsed}
onToggle={toggleRightSidebar}
/>
</div>
)}
</MetaBar>
);
};

Expand Down
45 changes: 43 additions & 2 deletions apps/site/components/withSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import Sidebar from '@node-core/ui-components/Containers/Sidebar';
import { useTranslations } from 'next-intl';
import { useRef } from 'react';

import CollapsedSidebarRail from '#site/components/Common/CollapsedSidebarRail';
import SidebarToggleButton from '#site/components/Common/SidebarToggleButton';
import Link from '#site/components/Link';
import useClientContext from '#site/hooks/useClientContext';
import useScrollToElement from '#site/hooks/useScrollToElement';
import useSiteNavigation from '#site/hooks/useSiteNavigation';
import { useRouter, usePathname } from '#site/navigation.mjs';
import { useSidebarState } from '#site/providers/sidebarStateProvider';

import type { FormattedMessage, NavigationKeys } from '#site/types';
import type { RichTranslationValues } from 'next-intl';
Expand All @@ -17,6 +20,7 @@ import type { FC } from 'react';
type WithSidebarProps = {
navKeys: Array<NavigationKeys>;
context?: Record<string, RichTranslationValues>;
disableToggle?: boolean;
};

type MappedItem = {
Expand All @@ -40,14 +44,20 @@ const mapItem = ([, item]: [string, MappedItem]): SidebarMappedEntry => ({
items: item.items ? item.items.map(mapItem) : [],
});

const WithSidebar: FC<WithSidebarProps> = ({ navKeys, context, ...props }) => {
const WithSidebar: FC<WithSidebarProps> = ({
navKeys,
context,
disableToggle = false,
...props
}) => {
const { getSideNavigation } = useSiteNavigation();
const pathname = usePathname()!;
const t = useTranslations();
const { push } = useRouter();
const { frontmatter } = useClientContext();
const sidebarRef = useRef<HTMLElement>(null);
const sideNavigation = getSideNavigation(navKeys, context);
const { isLeftSidebarCollapsed, toggleLeftSidebar } = useSidebarState();

// Preserve sidebar scroll position across navigations
useScrollToElement('sidebar', sidebarRef);
Expand All @@ -62,6 +72,27 @@ const WithSidebar: FC<WithSidebarProps> = ({ navKeys, context, ...props }) => {
})
);

const hasNavigationContent =
mappedSidebarItems.length > 0 && navKeys.length > 0;

// If no navigation content, render empty div to preserve grid structure
if (!hasNavigationContent) {
return <div />;
}

// Show collapsed rail when sidebar is collapsed and toggle is enabled
if (isLeftSidebarCollapsed && !disableToggle) {
return (
<CollapsedSidebarRail side="left">
<SidebarToggleButton
side="left"
isCollapsed={isLeftSidebarCollapsed}
onToggle={toggleLeftSidebar}
/>
</CollapsedSidebarRail>
);
}

return (
<Sidebar
ref={sidebarRef}
Expand All @@ -72,7 +103,17 @@ const WithSidebar: FC<WithSidebarProps> = ({ navKeys, context, ...props }) => {
onSelect={push}
as={Link}
{...props}
/>
>
{!disableToggle && (
<div className="mb-1 flex justify-end pr-2">
<SidebarToggleButton
side="left"
isCollapsed={isLeftSidebarCollapsed}
onToggle={toggleLeftSidebar}
/>
</div>
)}
</Sidebar>
);
};

Expand Down
11 changes: 5 additions & 6 deletions apps/site/layouts/About.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Article from '@node-core/ui-components/Containers/Article';

import ArticleWithSidebarState from '#site/components/Common/ArticleWithSidebarState';
import WithBreadcrumbs from '#site/components/withBreadcrumbs';
import WithFooter from '#site/components/withFooter';
import WithMetaBar from '#site/components/withMetaBar';
Expand All @@ -12,19 +11,19 @@ const AboutLayout: FC<PropsWithChildren> = ({ children }) => (
<>
<WithNavBar />

<Article>
<WithSidebar navKeys={['about', 'getInvolved']} />
<ArticleWithSidebarState>
<WithSidebar navKeys={['about', 'getInvolved']} disableToggle />

<div>
<main id="main" tabIndex={-1}>
{children}
</main>

<WithMetaBar />
<WithMetaBar disableToggle />
</div>

<WithBreadcrumbs navKeys={['about', 'getInvolved']} />
</Article>
</ArticleWithSidebarState>

<WithFooter />
</>
Expand Down
7 changes: 3 additions & 4 deletions apps/site/layouts/ArticlePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Article from '@node-core/ui-components/Containers/Article';

import ArticleWithSidebarState from '#site/components/Common/ArticleWithSidebarState';
import WithFooter from '#site/components/withFooter';
import WithMetaBar from '#site/components/withMetaBar';
import WithNavBar from '#site/components/withNavBar';
Expand All @@ -11,7 +10,7 @@ const ArticlePageLayout: FC<PropsWithChildren> = ({ children }) => (
<>
<WithNavBar />

<Article>
<ArticleWithSidebarState>
<WithSidebar navKeys={[]} />

<div>
Expand All @@ -21,7 +20,7 @@ const ArticlePageLayout: FC<PropsWithChildren> = ({ children }) => (

<WithMetaBar />
</div>
</Article>
</ArticleWithSidebarState>

<WithFooter />
</>
Expand Down
Loading