From aef1152edd54416bd90255cbb366737979100edc Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 7 Apr 2026 13:19:36 +0530 Subject: [PATCH 1/4] feat(client): add nested sections support with auto-expansion in sidebar --- apps/site/components/withSidebar.tsx | 27 +++++++- .../Sidebar/SidebarGroup/index.module.css | 47 ++++++++++++++ .../Containers/Sidebar/SidebarGroup/index.tsx | 62 ++++++++++++++++--- .../src/Containers/Sidebar/index.tsx | 20 +++++- 4 files changed, 144 insertions(+), 12 deletions(-) diff --git a/apps/site/components/withSidebar.tsx b/apps/site/components/withSidebar.tsx index 91866828ac934..b6d17fc62fa96 100644 --- a/apps/site/components/withSidebar.tsx +++ b/apps/site/components/withSidebar.tsx @@ -9,7 +9,7 @@ import { useClientContext, useScrollToElement } from '#site/hooks/client'; import { useSiteNavigation } from '#site/hooks/generic'; import { useRouter, usePathname } from '#site/navigation.mjs'; -import type { NavigationKeys } from '#site/types'; +import type { FormattedMessage, NavigationKeys } from '#site/types'; import type { RichTranslationValues } from 'next-intl'; import type { FC } from 'react'; @@ -18,6 +18,27 @@ type WithSidebarProps = { context?: Record; }; +type MappedItem = { + label: FormattedMessage; + link: string; + target?: string; + items?: Array<[string, MappedItem]>; +}; + +type SidebarMappedEntry = { + label: FormattedMessage; + link: string; + target?: string; + items?: Array; +}; + +const mapItem = ([, item]: [string, MappedItem]): SidebarMappedEntry => ({ + label: item.label, + link: item.link, + target: item.target, + items: item.items ? item.items.map(mapItem) : [], +}); + const WithSidebar: FC = ({ navKeys, context, ...props }) => { const { getSideNavigation } = useSiteNavigation(); const pathname = usePathname()!; @@ -34,9 +55,9 @@ const WithSidebar: FC = ({ navKeys, context, ...props }) => { // If there's only a single navigation key, use its sub-items // as our navigation. (navKeys.length === 1 ? sideNavigation[0][1].items : sideNavigation).map( - ([, { label, items }]) => ({ + ([, { label, items }]: [string, MappedItem]) => ({ groupName: label, - items: items.map(([, item]) => item), + items: items ? items.map(mapItem) : [], }) ); diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css index 0cecc21978828..52e031715126a 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.module.css @@ -21,4 +21,51 @@ text-neutral-800 dark:text-neutral-600; } + + .subGroup { + @apply flex + w-full + flex-col + gap-1; + } + + .summary { + @apply flex + cursor-pointer + items-center + justify-between + rounded-md + px-2 + py-1 + text-sm + font-semibold + text-neutral-800 + select-none + hover:bg-neutral-100 + dark:text-neutral-200 + hover:dark:bg-neutral-900; + + list-style: none; + + &::-webkit-details-marker { + display: none; + } + } + + .subGroup[open] .summary { + @apply text-green-600 + dark:text-green-400; + } + + .subItemList { + @apply mt-1 + ml-2 + flex + flex-col + gap-1 + border-l + border-neutral-200 + pl-2 + dark:border-neutral-800; + } } diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx index e156939474058..cad19b7d56272 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx @@ -7,27 +7,75 @@ import type { ComponentProps, FC } from 'react'; import styles from './index.module.css'; +type SidebarItemType = Omit< + ComponentProps, + 'as' | 'pathname' +> & { + items?: Array; +}; + type SidebarGroupProps = { groupName: FormattedMessage; - items: Array, 'as' | 'pathname'>>; + items: Array; as?: LinkLike; pathname?: string; - className: string; + className?: string; +}; + +const hasActivePath = ( + items: Array, + pathname?: string +): boolean => { + return items.some( + item => + item.link === pathname || + (item.items && hasActivePath(item.items, pathname)) + ); +}; + +const renderItems = ( + items: Array, + props: { as?: LinkLike }, + pathname?: string +) => { + return items.map(({ label, link, items: subItems }) => { + if (subItems && subItems.length > 0) { + const isOpen = hasActivePath(subItems, pathname); + return ( +
+ {label} +
    + {renderItems(subItems, props, pathname)} +
+
+ ); + } + return ( + + ); + }); }; const SidebarGroup: FC = ({ groupName, items, className, + pathname, ...props }) => (
-
    - {items.map(({ label, link }) => ( - - ))} -
+
    {renderItems(items, props, pathname)}
); diff --git a/packages/ui-components/src/Containers/Sidebar/index.tsx b/packages/ui-components/src/Containers/Sidebar/index.tsx index b41c83e1630da..af9bc3a2f5f9e 100644 --- a/packages/ui-components/src/Containers/Sidebar/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/index.tsx @@ -3,11 +3,25 @@ import { forwardRef } from 'react'; import WithNoScriptSelect from '#ui/Common/Select/NoScriptSelect'; import SidebarGroup from '#ui/Containers/Sidebar/SidebarGroup'; -import type { LinkLike } from '#ui/types'; +import type { FormattedMessage, LinkLike } from '#ui/types'; import type { ComponentProps, PropsWithChildren } from 'react'; import styles from './index.module.css'; +type SidebarItemType = { + label: FormattedMessage; + link: string; + items?: Array; +}; + +const flattenItems = ( + items: Array +): Array => { + return items.flatMap((item: SidebarItemType) => + item.items && item.items.length ? flattenItems(item.items) : [item] + ); +}; + type SidebarProps = { groups: Array< Pick, 'items' | 'groupName'> @@ -23,7 +37,9 @@ const SideBar = forwardRef>( ({ groups, pathname, title, onSelect, as, children, placeholder }, ref) => { const selectItems = groups.map(({ items, groupName }) => ({ label: groupName, - items: items.map(({ label, link }) => ({ value: link, label })), + items: flattenItems(items as Array).map( + ({ label, link }) => ({ value: link, label }) + ), })); const currentItem = selectItems From 291ff060b400312d1b0b5aace10a2ba939164129 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 7 Apr 2026 19:00:16 +0530 Subject: [PATCH 2/4] fix: resolve sidebar key and DOM nesting issues --- .../Containers/Sidebar/SidebarGroup/index.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx index cad19b7d56272..d3ef50fe239a4 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx @@ -42,16 +42,14 @@ const renderItems = ( if (subItems && subItems.length > 0) { const isOpen = hasActivePath(subItems, pathname); return ( -
- {label} -
    - {renderItems(subItems, props, pathname)} -
-
+
  • +
    + {label} +
      + {renderItems(subItems, props, pathname)} +
    +
    +
  • ); } return ( From a50a492fb53af8aa2c7cf45c14d0e67c2fab3487 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 7 Apr 2026 19:13:59 +0530 Subject: [PATCH 3/4] fix: update stories for nested sidebar structure --- .../Sidebar/SidebarGroup/index.stories.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx index 898e61bec7fe2..cca25d7f9c34c 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.stories.tsx @@ -33,4 +33,27 @@ export const EmptyGroup: Story = { }, }; +export const NestedGroup: Story = { + args: { + groupName: 'Nested Group', + pathname: '/nested/folder-b/leaf-2', + items: [ + { label: 'Flat Item', link: '/nested/flat' }, + { + label: 'Folder A', + link: '/nested/folder-a', + items: [{ label: 'Leaf A.1', link: '/nested/folder-a/leaf-1' }], + }, + { + label: 'Folder B', + link: '/nested/folder-b', + items: [ + { label: 'Leaf B.1', link: '/nested/folder-b/leaf-1' }, + { label: 'Leaf B.2 (Active)', link: '/nested/folder-b/leaf-2' }, + ], + }, + ], + }, +}; + export default { component: SidebarGroup } as Meta; From a6325296c7131e2c5647d12b2e6a5d49bbd928a0 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 7 Apr 2026 22:03:39 +0530 Subject: [PATCH 4/4] fix: expand sidebar group when parent route is active --- .../ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx index d3ef50fe239a4..edff8979c9d3e 100644 --- a/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx +++ b/packages/ui-components/src/Containers/Sidebar/SidebarGroup/index.tsx @@ -40,7 +40,7 @@ const renderItems = ( ) => { return items.map(({ label, link, items: subItems }) => { if (subItems && subItems.length > 0) { - const isOpen = hasActivePath(subItems, pathname); + const isOpen = link === pathname || hasActivePath(subItems, pathname); return (