diff --git a/src/constant/constants.ts b/src/constant/constants.ts
index 4f90781515..83af350747 100644
--- a/src/constant/constants.ts
+++ b/src/constant/constants.ts
@@ -37,8 +37,8 @@ export const navigationCardsData = [
// Keep items sorted alphabetically by `title`
export const techDomain = [
- { id: 'appdev', title: 'Application Dev. & Automation', icon: 'sap-icon://syntax' },
{ id: 'ai', title: 'AI & Machine Learning', icon: 'sap-icon://da' },
+ { id: 'appdev', title: 'Application Dev. & Automation', icon: 'sap-icon://syntax' },
{ id: 'data', title: 'Data & Analytics', icon: 'sap-icon://database' },
{ id: 'integration', title: 'Integration', icon: 'sap-icon://exit-full-screen' },
{ id: 'opsec', title: 'Operation & Security', icon: 'sap-icon://shield' },
diff --git a/src/css/custom.css b/src/css/custom.css
index 1c83ec294d..50b6546c7c 100644
--- a/src/css/custom.css
+++ b/src/css/custom.css
@@ -39,6 +39,7 @@
--ifm-footer-link-hover-color: #6b84a0;
--ifm-footer-title-color: var(--ifm-font-color-base);
--doc-sidebar-width: 350px !important;
+ --doc-sidebar-hidden-width: 30px;
--ifm-table-cell-padding: 0.25rem 0.5rem;
--ifm-menu-link-padding-vertical: 0.375rem;
--ifm-menu-link-padding-horizontal: 0.75rem;
@@ -363,10 +364,6 @@ svg[aria-roledescription="flowchart-v2"] span {
/* Remove bullet points */
}
-.menu__list-item>.menu__link--active {
- background-color: var(--ifm-sidebar-selected-item-background-color);
-}
-
.menu__link {
/* Changed by PO: All pages should use the same colors */
color: #535353;
@@ -376,6 +373,23 @@ svg[aria-roledescription="flowchart-v2"] span {
line-height: 1rem;
}
+/* Align domain category arrows with nested level arrows */
+/* Target domain-level links by the --sublist-caret modifier */
+a.menu__link--sublist-caret {
+ padding-right: 0.1875rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+}
+
+/* Keep domain category arrows fixed on hover - only shift the text */
+.menu__link--sublist-caret:hover {
+ transform: none !important;
+}
+
+.menu__link--sublist-caret:hover [class*="categoryLinkLabel"] {
+ transform: translateX(4px);
+}
+
/* Changing the padding size of the symbol to expand the content in the sidebar */
.menu__caret {
padding: 0.2rem !important;
@@ -1372,33 +1386,84 @@ html[data-theme='dark'] .theme-doc-toc-desktop {
}
/* Interactive Sidebar Links */
-.theme-doc-sidebar-menu .menu__link {
+.theme-doc-sidebar-menu .menu__link,
+nav[class*="domainSidebar"] .menu__link {
transition: transform 0.3s ease, color 0.3s ease, text-shadow 0.3s ease;
}
-.theme-doc-sidebar-menu .menu__link:hover {
+.theme-doc-sidebar-menu .menu__link:hover,
+nav[class*="domainSidebar"] .menu__link:hover {
transform: translateX(4px);
color: var(--ifm-color-primary) !important;
}
+/* Change [+N] counter color on link hover - Light mode only */
+html[data-theme='light'] .theme-doc-sidebar-menu .menu__link:hover .sidebar-duplicate-counter,
+html[data-theme='light'] nav[class*="domainSidebar"] .menu__link:hover .sidebar-duplicate-counter {
+ color: var(--ifm-color-primary) !important;
+}
+
+/* Prevent [+N] counter color change on hover in dark mode */
+html[data-theme='dark'] .sidebar-duplicate-counter:hover {
+ color: var(--ifm-color-content-secondary) !important;
+}
+
/* All active links get bold */
-.theme-doc-sidebar-menu .menu__link--active {
+.theme-doc-sidebar-menu .menu__link--active,
+nav[class*="domainSidebar"] .menu__link--active {
font-weight: bold;
}
+/* Active document links (not folders) - hover animation combining both transforms */
+.menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active:hover {
+ transform: translateX(4px) translateZ(0) !important;
+}
+
/* Gradient effect on document links (all documents, not folders) */
-.theme-doc-sidebar-menu .menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active {
- background: var(--gradient-premium);
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
+/* Generic selector that works for desktop, mobile, both themes */
+.menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active {
+ color: var(--ifm-color-primary) !important;
+ background: var(--gradient-premium) !important;
+ background-color: transparent !important;
+ -webkit-background-clip: text !important;
+ background-clip: text !important;
+ -webkit-text-fill-color: transparent !important;
+ font-weight: bold !important;
+ /* Force hardware acceleration and separate layer to ensure gradient renders */
+ transform: translateZ(0);
+ will-change: background;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+}
+
+/* Dark theme - ensure gradient variable works */
+html[data-theme='dark'] .menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active {
+ color: var(--ifm-color-primary) !important;
+ background: var(--gradient-premium) !important;
+ background-color: transparent !important;
+ -webkit-background-clip: text !important;
+ background-clip: text !important;
+ -webkit-text-fill-color: transparent !important;
+ font-weight: bold !important;
+ transform: translateZ(0);
+ will-change: background;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+}
+
+/* Fallback for browsers that don't support background-clip: text */
+@supports not (background-clip: text) {
+ .menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active {
+ color: var(--ifm-color-primary) !important;
+ -webkit-text-fill-color: var(--ifm-color-primary) !important;
+ background: none !important;
+ }
}
/* Folder nodes - default light theme */
.theme-doc-sidebar-menu .menu__list-item-collapsible > .menu__link,
.menu__list-item-collapsible > .menu__link {
color: #535353;
- -webkit-text-fill-color: #535353;
}
/* Parent folder (expanded but NOT selected) - light theme */
@@ -1417,6 +1482,11 @@ html[data-theme='dark'] .theme-doc-toc-desktop {
background-clip: text !important;
-webkit-text-fill-color: transparent !important;
font-weight: bold !important;
+ /* Force hardware acceleration and separate layer to ensure gradient renders */
+ transform: translateZ(0);
+ will-change: background;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
}
/* Folder nodes - default dark theme */
@@ -1442,12 +1512,38 @@ html[data-theme='dark'] .menu__list-item-collapsible--active > .menu__link.menu_
background-clip: text !important;
-webkit-text-fill-color: transparent !important;
font-weight: bold !important;
+ /* Force hardware acceleration and separate layer to ensure gradient renders */
+ transform: translateZ(0);
+ will-change: background;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
}
-html[data-theme='dark'] .theme-doc-sidebar-menu .menu__link:hover {
+html[data-theme='dark'] .theme-doc-sidebar-menu .menu__link:hover,
+html[data-theme='dark'] nav[class*="domainSidebar"] .menu__link:hover {
+ transform: translateX(4px);
text-shadow: 0 0 8px rgba(0, 112, 242, 0.6);
}
+/* Disable slide animation and color change on mobile - light theme only */
+@media (max-width: 996px) {
+ .theme-doc-sidebar-menu .menu__link:hover,
+ nav[class*="domainSidebar"] .menu__link:hover,
+ html[data-theme='dark'] .theme-doc-sidebar-menu .menu__link:hover,
+ html[data-theme='dark'] nav[class*="domainSidebar"] .menu__link:hover,
+ .menu__link--sublist-caret:hover [class*="categoryLinkLabel"],
+ .menu__list-item:not(.menu__list-item-collapsible) > .menu__link--active:hover {
+ transform: none !important;
+ }
+
+ /* Disable color change on mobile in light theme */
+ html[data-theme='light'] .theme-doc-sidebar-menu .menu__link:hover,
+ html[data-theme='light'] nav[class*="domainSidebar"] .menu__link:hover,
+ html[data-theme='light'] .menu__link:hover .sidebar-duplicate-counter {
+ color: inherit !important;
+ }
+}
+
/* Content Container Card */
.theme-doc-markdown {
background: transparent;
@@ -1603,6 +1699,7 @@ body:has(.homepage-main) footer.footer {
}
/* On mobile, allow normal scrolling */
+
@media (max-width: 768px) {
.homepage-main {
scroll-snap-type: none !important;
diff --git a/src/plugins/security-headers/index.js b/src/plugins/security-headers/index.js
index a02736e68b..5bc9b59c1c 100644
--- a/src/plugins/security-headers/index.js
+++ b/src/plugins/security-headers/index.js
@@ -50,13 +50,6 @@ module.exports = function (_context, _options) {
content: 'nosniff',
},
},
- {
- tagName: 'meta',
- attributes: {
- 'http-equiv': 'X-Frame-Options',
- content: 'DENY',
- },
- },
{
tagName: 'meta',
attributes: {
diff --git a/src/sections/TechnologyDomainSection.tsx b/src/sections/TechnologyDomainSection.tsx
index 299fcb0ff5..16b0168b58 100644
--- a/src/sections/TechnologyDomainSection.tsx
+++ b/src/sections/TechnologyDomainSection.tsx
@@ -23,22 +23,22 @@ interface DomainCardProps {
title: string;
icon: string;
};
+ onNavigationStart: () => void;
}
-function DomainCard({ domain }: DomainCardProps): JSX.Element {
- const setTechDomains = useSidebarFilterStore((state) => state.setTechDomains);
+function DomainCard({ domain, onNavigationStart }: DomainCardProps): JSX.Element {
const docsUrl = useBaseUrl('/docs/ref-arch');
const isHighlighted = domain.id === 'ai' || domain.id === 'data';
- const handleClick = () => {
- setTechDomains([domain.id]);
+ const handlePointerDown = () => {
+ onNavigationStart();
};
return (
{iconMap[domain.id] ||
}
@@ -113,7 +113,30 @@ export default function TechnologyDomainSection(): JSX.Element {
const imgBaseUrl = useBaseUrl('/img/landingPage/');
const history = useHistory();
const setPartners = useSidebarFilterStore((state) => state.setPartners);
- const setTechDomains = useSidebarFilterStore((state) => state.setTechDomains);
+ const [isNavigating, setIsNavigating] = React.useState(false);
+
+ // Disable scroll-snap when navigating to prevent interference
+ React.useEffect(() => {
+ if (isNavigating) {
+ const originalHtmlSnap = document.documentElement.style.scrollSnapType;
+ const originalBodySnap = document.body.style.scrollSnapType;
+
+ document.documentElement.style.scrollSnapType = 'none';
+ document.body.style.scrollSnapType = 'none';
+
+ const timer = setTimeout(() => {
+ document.documentElement.style.scrollSnapType = originalHtmlSnap;
+ document.body.style.scrollSnapType = originalBodySnap;
+ setIsNavigating(false);
+ }, 300);
+
+ return () => {
+ clearTimeout(timer);
+ document.documentElement.style.scrollSnapType = originalHtmlSnap;
+ document.body.style.scrollSnapType = originalBodySnap;
+ };
+ }
+ }, [isNavigating]);
// Helper function to get image URL with baseUrl
const getImg = (name: string) => `${imgBaseUrl}${name}`;
@@ -124,16 +147,13 @@ export default function TechnologyDomainSection(): JSX.Element {
e.preventDefault();
const partners = item.filter?.partners ?? [];
- const techDomains = item.filter?.techDomains ?? [];
- // Set the global store
+ // Set the global store - only partners filter now
if (partners.length) setPartners(partners);
- if (techDomains.length) setTechDomains(techDomains);
- // Build query string
+ // Build query string - only partners
const params = new URLSearchParams();
if (partners.length) params.set('partners', partners.join(','));
- if (techDomains.length) params.set('techDomains', techDomains.join(','));
history.push(`${docsUrl}?${params.toString()}`);
};
@@ -160,7 +180,11 @@ export default function TechnologyDomainSection(): JSX.Element {
{techDomain.map((domain) => (
-
+ setIsNavigating(true)}
+ />
))}
@@ -183,4 +207,4 @@ export default function TechnologyDomainSection(): JSX.Element {
);
-}
+}
\ No newline at end of file
diff --git a/src/store/sidebar-store.ts b/src/store/sidebar-store.ts
index a4b8b6d8c3..2bdafb91b6 100644
--- a/src/store/sidebar-store.ts
+++ b/src/store/sidebar-store.ts
@@ -7,6 +7,10 @@ interface SidebarFilterState {
partners: string[];
setPartners: (partners: string[]) => void;
+ // Expanded domain categories (for collapsible sidebar)
+ expandedDomains: string[];
+ setExpandedDomains: (domains: string[]) => void;
+
resetFilters: () => void;
}
@@ -17,5 +21,9 @@ export const useSidebarFilterStore = create
((set) => ({
partners: [],
setPartners: (partners) => set({ partners }),
- resetFilters: () => set({ techDomains: [], partners: [] }),
+ // Start with all domains collapsed by default
+ expandedDomains: [],
+ setExpandedDomains: (expandedDomains) => set({ expandedDomains }),
+
+ resetFilters: () => set({ techDomains: [], partners: [], expandedDomains: [] }),
}));
diff --git a/src/theme/DocSidebar/index.tsx b/src/theme/DocSidebar/index.tsx
index f2ea4b623f..0da0b5eabd 100644
--- a/src/theme/DocSidebar/index.tsx
+++ b/src/theme/DocSidebar/index.tsx
@@ -1,17 +1,28 @@
import React, { useMemo, useEffect, useState } from 'react';
+import clsx from 'clsx';
import DocSidebar from '@theme-original/DocSidebar';
import DocSidebarItems from '@theme-original/DocSidebarItems';
-import { NavbarSecondaryMenuFiller, useWindowSize } from '@docusaurus/theme-common';
+import { NavbarSecondaryMenuFiller, useWindowSize, useThemeConfig } from '@docusaurus/theme-common';
import { useDocsSidebar } from '@docusaurus/plugin-content-docs/client';
import CollapsibleFilterBar from '@site/src/components/FilterBar/CollapsibleFilterBar';
+import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton';
import styles from './styles.module.css';
import { useSidebarFilterStore } from '@site/src/store/sidebar-store';
import useGlobalData from '@docusaurus/useGlobalData';
import tagsMap from '@site/src/constant/tagsMapping.json';
-import { useHistory } from '@docusaurus/router';
+import { useHistory, useLocation } from '@docusaurus/router';
import useBaseUrl from '@docusaurus/useBaseUrl';
import { logger } from '@site/src/utils/logger';
+// Domain definitions with labels
+const DOMAIN_DEFINITIONS = [
+ { id: 'ai', label: 'AI & Machine Learning' },
+ { id: 'appdev', label: 'Application Dev. & Automation' },
+ { id: 'data', label: 'Data & Analytics' },
+ { id: 'integration', label: 'Integration' },
+ { id: 'opsec', label: 'Operation & Security' },
+];
+
const categoryIdToTags = Object.entries(tagsMap).reduce((acc, [tagKey, meta]) => {
const cat = meta?.categoryid;
if (!cat) return acc;
@@ -82,18 +93,291 @@ function filterSidebarItems(items, selectedDomains, selectedPartners, docIdToTag
return recurse(items);
}
+// Helper to count occurrences of a docId in items (recursive)
+function countDocsInItems(items, docId): number {
+ let count = 0;
+ for (const item of items) {
+ if ((item.type === 'doc' || item.type === 'link') && (item.docId === docId || item.id === docId)) {
+ count++;
+ } else if (item.type === 'category') {
+ // Check if the category itself matches (for parent architectures with href)
+ if (item.href === docId) {
+ count++;
+ }
+ // Recursively check children
+ if (item.items) {
+ count += countDocsInItems(item.items, docId);
+ }
+ }
+ }
+ return count;
+}
+
+// Helper to count total docs in items (recursive) - for badge display
+function countTotalDocsInItems(items): number {
+ let count = 0;
+ for (const item of items) {
+ if (item.type === 'doc' || item.type === 'link') {
+ count++;
+ } else if (item.type === 'category') {
+ // Count the category itself if it has a link (parent architecture that's also a document)
+ if (item.link || item.docId || item.href) {
+ count++;
+ }
+ // Also count children recursively
+ if (item.items) {
+ count += countTotalDocsInItems(item.items);
+ }
+ }
+ }
+ return count;
+}
+
+// Group sidebar items by technology domain (preserving hierarchy)
+function groupSidebarByDomain(items, docIdToTags) {
+ const domainIds = DOMAIN_DEFINITIONS.map((d) => d.id);
+ const grouped: Record = {};
+ const duplicateCounts: Record = {};
+
+ // Initialize empty arrays for each domain
+ domainIds.forEach((id) => { grouped[id] = []; });
+
+ // Helper: Check if a doc/link belongs to a domain
+ const itemBelongsToDomain = (item, domainId) => {
+ const itemId = item.docId || item.id || '';
+ const tags = docIdToTags?.[itemId] || [];
+
+ // Direct match
+ if (tags.includes(domainId)) return true;
+
+ // Check category mappings
+ const domainTags = categoryIdToTags[domainId] || [];
+ return domainTags.some((tag) => tags.includes(tag));
+ };
+
+ // Helper: Recursively check if a category contains any docs for this domain
+ const categoryHasDocsForDomain = (category, domainId): boolean => {
+ if (!category.items || category.items.length === 0) return false;
+
+ for (const child of category.items) {
+ if ((child.type === 'doc' || child.type === 'link') && itemBelongsToDomain(child, domainId)) {
+ return true;
+ }
+ if (child.type === 'category' && categoryHasDocsForDomain(child, domainId)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // First pass: Collect ALL document IDs (including parent architectures) from the entire sidebar
+ const collectAllDocIds = (itemList: any[]): Set => {
+ const docIds = new Set();
+
+ const traverse = (item: any) => {
+ if (item.type === 'doc' || item.type === 'link') {
+ const docId = item.docId || item.id;
+ if (docId) docIds.add(docId);
+ } else if (item.type === 'category') {
+ // If category has href and children, it's a parent architecture (expandable document)
+ if (item.href && item.items && item.items.length > 0) {
+ docIds.add(item.href);
+ }
+ // Recursively process children
+ if (item.items) {
+ item.items.forEach(traverse);
+ }
+ }
+ };
+
+ itemList.forEach(traverse);
+ return docIds;
+ };
+
+ // Collect all doc IDs first
+ const allDocIds = collectAllDocIds(items);
+
+ // Helper: Recursively filter category items by domain
+ const filterCategoryForDomain = (category, domainId) => {
+ const filteredItems = [];
+
+ for (const child of category.items || []) {
+ if ((child.type === 'doc' || child.type === 'link') && itemBelongsToDomain(child, domainId)) {
+ filteredItems.push(child);
+ } else if (child.type === 'category' && categoryHasDocsForDomain(child, domainId)) {
+ filteredItems.push(filterCategoryForDomain(child, domainId));
+ }
+ }
+
+ return { ...category, items: filteredItems };
+ };
+
+ // Group items by domain, preserving category structure
+ items.forEach((item) => {
+ domainIds.forEach((domainId) => {
+ if ((item.type === 'doc' || item.type === 'link') && itemBelongsToDomain(item, domainId)) {
+ grouped[domainId].push(item);
+ } else if (item.type === 'category' && categoryHasDocsForDomain(item, domainId)) {
+ const filteredCategory = filterCategoryForDomain(item, domainId);
+ grouped[domainId].push(filteredCategory);
+ }
+ });
+ });
+
+ // Calculate duplicate counts for all doc IDs
+ allDocIds.forEach((docId) => {
+ let count = 0;
+ domainIds.forEach((domainId) => {
+ const hasDoc = countDocsInItems(grouped[domainId], docId) > 0;
+ if (hasDoc) count++;
+ });
+ if (count > 1) {
+ duplicateCounts[docId] = count - 1;
+ }
+ });
+
+ return { grouped, duplicateCounts };
+}
+
+// Filter grouped items by partner (preserving hierarchy)
+function filterGroupedByPartner(grouped, selectedPartners, docIdToTags) {
+ if (!selectedPartners?.length) return grouped;
+
+ const expand = (ids) =>
+ Array.from(new Set(ids.flatMap((id) => [id, ...(categoryIdToTags[id] ?? [])])));
+ const partnerTags = expand(selectedPartners);
+
+ // Helper: Check if item matches partner filter
+ const itemMatchesPartner = (item) => {
+ const itemId = item.docId || item.id || '';
+ const tags = docIdToTags?.[itemId] || [];
+ return partnerTags.some((p) => tags.includes(p));
+ };
+
+ // Helper: Recursively filter category
+ const filterCategory = (category) => {
+ const filteredItems = [];
+ for (const child of category.items || []) {
+ if ((child.type === 'doc' || child.type === 'link') && itemMatchesPartner(child)) {
+ filteredItems.push(child);
+ } else if (child.type === 'category') {
+ const filteredChild = filterCategory(child);
+ if (filteredChild.items.length > 0) {
+ filteredItems.push(filteredChild);
+ }
+ }
+ }
+ return { ...category, items: filteredItems };
+ };
+
+ const filtered: Record = {};
+ Object.entries(grouped).forEach(([domainId, items]) => {
+ filtered[domainId] = [];
+ for (const item of items) {
+ if ((item.type === 'doc' || item.type === 'link') && itemMatchesPartner(item)) {
+ filtered[domainId].push(item);
+ } else if (item.type === 'category') {
+ const filteredCategory = filterCategory(item);
+ if (filteredCategory.items.length > 0) {
+ filtered[domainId].push(filteredCategory);
+ }
+ }
+ }
+ });
+
+ return filtered;
+}
+
// ============================================================================
-// Desktop Version
+// Shared Helper Functions
// ============================================================================
-// Constant options defined outside component to avoid recreating on each render
-const TECH_DOMAIN_OPTIONS = [
- { value: 'ai', label: 'AI & Machine Learning' },
- { value: 'appdev', label: 'Application Dev. & Automation' },
- { value: 'data', label: 'Data & Analytics' },
- { value: 'integration', label: 'Integration' },
- { value: 'opsec', label: 'Operation & Security' }
-];
+// Collect unique doc IDs from grouped items (for result count display)
+function collectUniqueDocIds(groupedItems: Record): Set {
+ const uniqueDocIds = new Set();
+
+ const traverse = (items: any[]) => {
+ items.forEach(item => {
+ if (item.type === 'doc' || item.type === 'link') {
+ const id = item.docId || item.id || '';
+ if (id) uniqueDocIds.add(id);
+ } else if (item.type === 'category') {
+ // Count the category itself if it has href (parent architecture)
+ if (item.href) {
+ uniqueDocIds.add(item.href);
+ }
+ // Recursively count children
+ if (item.items) {
+ traverse(item.items);
+ }
+ }
+ });
+ };
+
+ Object.values(groupedItems).forEach(items => traverse(items));
+ return uniqueDocIds;
+}
+
+// Add duplicate counters to item customProps recursively
+function addDuplicateCountersToItems(items: any[], duplicateCounts: Record): any[] {
+ return items.map(item => {
+ if (item.type === 'category') {
+ // For parent architectures (categories with href), use href for matching
+ const categoryId = item.href || item.docId || item.id || '';
+ const categoryDuplicateCount = duplicateCounts[categoryId];
+
+ return {
+ ...item,
+ customProps: {
+ ...item.customProps,
+ ...(categoryDuplicateCount && { duplicateCount: categoryDuplicateCount })
+ },
+ items: item.items ? addDuplicateCountersToItems(item.items, duplicateCounts) : []
+ };
+ } else if (item.type === 'doc' || item.type === 'link') {
+ const itemId = item.docId || item.id || '';
+ const duplicateCount = duplicateCounts[itemId];
+
+ return {
+ ...item,
+ customProps: {
+ ...item.customProps,
+ ...(duplicateCount && { duplicateCount })
+ }
+ };
+ }
+
+ return item;
+ });
+}
+
+// Transform domain-grouped data into Docusaurus category structure
+function buildDomainCategories(
+ filteredGrouped: Record,
+ duplicateCounts: Record,
+ expandedDomains: string[]
+) {
+ return DOMAIN_DEFINITIONS.map(domain => {
+ const domainItems = filteredGrouped[domain.id] || [];
+ const docCount = countTotalDocsInItems(domainItems);
+ const itemsWithCounters = addDuplicateCountersToItems(domainItems, duplicateCounts);
+
+ return {
+ type: 'category',
+ label: `${domain.label} (${docCount})`,
+ items: itemsWithCounters,
+ collapsible: true,
+ collapsed: !expandedDomains.includes(domain.id),
+ customProps: {
+ domainId: domain.id
+ }
+ };
+ }).filter(category => category.items.length > 0);
+}
+
+// ============================================================================
+// Desktop Version
+// ============================================================================
const PARTNER_OPTIONS = [
{ value: 'aws', label: 'Amazon Web Services' },
{ value: 'azure', label: 'Microsoft Azure' },
@@ -108,26 +392,34 @@ function DocSidebarDesktop(props) {
const tagsDocId = useGlobalData()['docusaurus-tags-plugin'].default?.docIdToTags;
const sidebar = useDocsSidebar();
const shouldShowFilters = sidebar?.name === 'refarchSidebar';
+ const location = useLocation();
+ const {
+ navbar: { hideOnScroll },
+ docs: {
+ sidebar: { hideable },
+ },
+ } = useThemeConfig();
- const techDomains = useSidebarFilterStore((state) => state.techDomains);
- const setTechDomains = useSidebarFilterStore((state) => state.setTechDomains);
const partners = useSidebarFilterStore((state) => state.partners);
const setPartners = useSidebarFilterStore((state) => state.setPartners);
const resetFilters = useSidebarFilterStore((state) => state.resetFilters);
+ const expandedDomains = useSidebarFilterStore((state) => state.expandedDomains);
const [searchTerm, setSearchTerm] = useState('');
- // All hooks must be called before any conditional returns
- const filteredSidebar = useMemo(
- () => filterSidebarItems(props.sidebar, techDomains, partners, tagsDocId),
- [props.sidebar, techDomains, partners, tagsDocId]
+ // Group sidebar items by domain
+ const grouped = useMemo(
+ () => groupSidebarByDomain(props.sidebar, tagsDocId),
+ [props.sidebar, tagsDocId]
);
- // Convert string arrays to Option arrays
- const selectedTechDomainOptions = useMemo(
- () => TECH_DOMAIN_OPTIONS.filter(opt => techDomains.includes(opt.value)),
- [techDomains]
+ // Filter by selected partners
+ const filteredGrouped = useMemo(
+ () => filterGroupedByPartner(grouped.grouped, partners, tagsDocId),
+ [grouped.grouped, partners, tagsDocId]
);
+
+ // Convert string arrays to Option arrays
const selectedPartnerOptions = useMemo(
() => PARTNER_OPTIONS.filter(opt => partners.includes(opt.value)),
[partners]
@@ -137,17 +429,6 @@ function DocSidebarDesktop(props) {
return ;
}
- const handleTechDomainsChange = (selected) => {
- const selectedKeys = selected.map(opt => opt.value);
- setTechDomains(selectedKeys);
-
- // Sync URL
- const params = new URLSearchParams(location.search);
- if (selectedKeys.length) params.set('techDomains', selectedKeys.join(','));
- else params.delete('techDomains');
- window.history.replaceState({}, '', `${location.pathname}?${params.toString()}`);
- };
-
const handlePartnersChange = (selected) => {
const selectedKeys = selected.map(opt => opt.value);
setPartners(selectedKeys);
@@ -156,6 +437,7 @@ function DocSidebarDesktop(props) {
const params = new URLSearchParams(location.search);
if (selectedKeys.length) params.set('partners', selectedKeys.join(','));
else params.delete('partners');
+ params.delete('techDomains'); // Remove old techDomains param
window.history.replaceState({}, '', `${location.pathname}?${params.toString()}`);
};
@@ -165,43 +447,50 @@ function DocSidebarDesktop(props) {
window.history.replaceState({}, '', location.pathname);
};
- // Count total filtered docs
- const countDocs = (items) => {
- let count = 0;
- items.forEach(item => {
- if (item.type === 'doc' || item.type === 'link') {
- count++;
- } else if (item.type === 'category' && item.items) {
- count += countDocs(item.items);
- }
- });
- return count;
- };
+ // Count unique documents (across all domains, no duplicates)
+ const resultCount = collectUniqueDocIds(filteredGrouped).size;
- const resultCount = countDocs(filteredSidebar);
- const newProps = { ...props, sidebar: filteredSidebar };
+ // Transform domain-grouped data into Docusaurus category structure
+ const domainCategories = buildDomainCategories(
+ filteredGrouped,
+ grouped.duplicateCounts,
+ expandedDomains
+ );
return (
-
-
-
0 || partners.length > 0 || searchTerm.length > 0}
- searchTerm={searchTerm}
- onSearchChange={setSearchTerm}
- resultCount={resultCount}
+
+
+ 0 || searchTerm.length > 0}
+ searchTerm={searchTerm}
+ onSearchChange={setSearchTerm}
+ resultCount={resultCount}
+ />
+
+
+
+
-
-
-
+
+ {hideable &&
}
+
+
);
}
@@ -210,22 +499,27 @@ function DocSidebarDesktop(props) {
// ============================================================================
function FilteredMobileSidebarView({ sidebar, path, onItemClick }) {
const tagsDocId = useGlobalData()['docusaurus-tags-plugin'].default?.docIdToTags;
- const techDomains = useSidebarFilterStore((state) => state.techDomains);
- const setTechDomains = useSidebarFilterStore((state) => state.setTechDomains);
const partners = useSidebarFilterStore((state) => state.partners);
const setPartners = useSidebarFilterStore((state) => state.setPartners);
const resetFilters = useSidebarFilterStore((state) => state.resetFilters);
+ const expandedDomains = useSidebarFilterStore((state) => state.expandedDomains);
const [searchTerm, setSearchTerm] = useState('');
// Convert string arrays to Option arrays
- const selectedTechDomainOptions = TECH_DOMAIN_OPTIONS.filter(opt => techDomains.includes(opt.value));
const selectedPartnerOptions = PARTNER_OPTIONS.filter(opt => partners.includes(opt.value));
- const handleTechDomainsChange = (selected) => {
- const selectedKeys = selected.map(opt => opt.value);
- setTechDomains(selectedKeys);
- };
+ // Group sidebar items by domain
+ const grouped = useMemo(
+ () => groupSidebarByDomain(sidebar, tagsDocId),
+ [sidebar, tagsDocId]
+ );
+
+ // Filter by selected partners
+ const filteredGrouped = useMemo(
+ () => filterGroupedByPartner(grouped.grouped, partners, tagsDocId),
+ [grouped.grouped, partners, tagsDocId]
+ );
const handlePartnersChange = (selected) => {
const selectedKeys = selected.map(opt => opt.value);
@@ -238,42 +532,37 @@ function FilteredMobileSidebarView({ sidebar, path, onItemClick }) {
window.history.replaceState({}, '', location.pathname);
};
- const filteredSidebar = useMemo(
- () => filterSidebarItems(sidebar, techDomains, partners, tagsDocId),
- [sidebar, techDomains, partners, tagsDocId]
- );
-
- // Count total filtered docs
- const countDocs = (items) => {
- let count = 0;
- items.forEach(item => {
- if (item.type === 'doc' || item.type === 'link') {
- count++;
- } else if (item.type === 'category' && item.items) {
- count += countDocs(item.items);
- }
- });
- return count;
- };
+ // Count unique documents
+ const resultCount = collectUniqueDocIds(filteredGrouped).size;
- const resultCount = countDocs(filteredSidebar);
+ // Transform domain-grouped data into Docusaurus category structure
+ const domainCategories = buildDomainCategories(
+ filteredGrouped,
+ grouped.duplicateCounts,
+ expandedDomains
+ );
return (
<>
0 || partners.length > 0 || searchTerm.length > 0}
+ isResetEnabled={partners.length > 0 || searchTerm.length > 0}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
resultCount={resultCount}
/>
-
+
>
);
}
@@ -306,27 +595,74 @@ function DocSidebarMobile({ shouldShowFilters, ...props }) {
const DocSidebarDesktopMemo = React.memo(DocSidebarDesktop);
const DocSidebarMobileMemo = React.memo(DocSidebarMobile);
+// Helper function to find a doc in sidebar by path
+function findDocByPath(items, pathname) {
+ for (const item of items) {
+ if (item.type === 'doc' || item.type === 'link') {
+ if (item.href === pathname || pathname.startsWith(item.href)) {
+ return item.docId || item.id;
+ }
+ } else if (item.type === 'category' && item.items) {
+ const found = findDocByPath(item.items, pathname);
+ if (found) return found;
+ }
+ }
+ return null;
+}
+
export default function DocSidebarWrapper(props) {
const windowSize = useWindowSize();
const sidebarContext = useDocsSidebar();
const shouldShowFilters = sidebarContext?.name === 'refarchSidebar';
const setPartners = useSidebarFilterStore((state) => state.setPartners);
- const setTechDomains = useSidebarFilterStore((state) => state.setTechDomains);
+ const setExpandedDomains = useSidebarFilterStore((state) => state.setExpandedDomains);
const resetFilters = useSidebarFilterStore((state) => state.resetFilters);
const history = useHistory();
const docsBase = useBaseUrl('/docs');
+ const location = useLocation();
+ const tagsDocId = useGlobalData()['docusaurus-tags-plugin']?.default?.docIdToTags;
useEffect(() => {
if (!location.pathname.startsWith(docsBase)) return;
+ if (!shouldShowFilters) return; // Only run for ref-arch sidebar
+ if (!tagsDocId) return; // Wait for tags data to load
+ if (!props.sidebar) return; // Wait for sidebar to load
const params = new URLSearchParams(location.search);
-
const partnersParam = params.get('partners');
- const techDomainsParam = params.get('techDomains');
+ const expandedParam = params.get('expanded');
if (partnersParam) setPartners(partnersParam.split(','));
- if (techDomainsParam) setTechDomains(techDomainsParam.split(','));
- }, [docsBase, setPartners, setTechDomains]);
+
+ // If expanded param is set, use it (explicit choice from landing page)
+ if (expandedParam) {
+ setExpandedDomains(expandedParam.split(','));
+ return;
+ }
+
+ // Auto-expand domains for the current doc
+ // Find the doc ID by matching the current pathname to sidebar items
+ const docId = findDocByPath(props.sidebar, location.pathname);
+
+ // If we're on a specific doc page
+ if (docId && tagsDocId[docId]) {
+ const docTags = tagsDocId[docId] || [];
+ const domainIds = DOMAIN_DEFINITIONS.map((d) => d.id);
+
+ // Find which domains this doc belongs to
+ const matchingDomains = domainIds.filter((domainId) => {
+ // Direct match
+ if (docTags.includes(domainId)) return true;
+ // Check category mappings
+ const domainTags = categoryIdToTags[domainId] || [];
+ return domainTags.some((tag) => docTags.includes(tag));
+ });
+
+ if (matchingDomains.length > 0) {
+ setExpandedDomains(matchingDomains);
+ }
+ }
+ }, [location.pathname, location.search, docsBase, setPartners, setExpandedDomains, shouldShowFilters, tagsDocId, props.sidebar]);
useEffect(() => {
diff --git a/src/theme/DocSidebar/styles.module.css b/src/theme/DocSidebar/styles.module.css
index fd7662ea90..5d4b880403 100644
--- a/src/theme/DocSidebar/styles.module.css
+++ b/src/theme/DocSidebar/styles.module.css
@@ -1,7 +1,6 @@
-.refarchSidebarActive {
- height: 100%;
- display: flex;
- flex-direction: column;
+/* Make sidebarViewport take full width of its container */
+:global([class*="sidebarViewport"]) {
+ width: 100% !important;
}
.scrollableContent {
@@ -9,31 +8,10 @@
flex-grow: 1;
}
-:global(html[data-sidebar-collapsed='true']) .scrollableContent {
- display: none;
-}
-
-:global(html[data-sidebar-collapsed='true']) .sidebarWithFiltersContainer > div:first-child {
- display: none !important;
-}
-
-:global(.theme-doc-sidebar-container.theme-doc-sidebar-container-hidden) .sidebarWithFiltersContainer > div:first-child {
- display: none !important;
-}
-
-:global([class*="docSidebarContainer"][class*="Hidden"]) .sidebarWithFiltersContainer > div:first-child {
- display: none !important;
-}
-
-.theme-doc-sidebar-container-collapsed .custom-sidebar-filters-container {
- display: none;
-}
-
-.sidebarMenuList .menu.thin-scrollbar {
- flex: 1 1 auto;
- min-height: 0;
- overflow-y: auto;
- overflow-x: hidden;
+/* Hide filter bar and sidebar content when collapsed */
+.sidebarHidden {
+ opacity: 0 !important;
+ visibility: hidden !important;
}
.sidebarWithFiltersContainer {
@@ -51,6 +29,14 @@
flex-direction: column;
}
+.sidebar {
+ border-right: 1px solid var(--ifm-toc-border-color);
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: var(--doc-sidebar-width);
+}
+
[class^="sidebar_"] {
border-right: 1px solid var(--ifm-toc-border-color);
}
@@ -58,3 +44,58 @@
:global(button[class*="collapseSidebarButton"]) {
border-right: none !important;
}
+
+/* Ensure expand button icon is visible */
+:global([class*="expandButton"]) {
+ cursor: pointer;
+}
+
+:global([class*="expandButtonIcon"]) {
+ width: 20px;
+ height: 20px;
+ display: inline-block;
+}
+
+/* Domain Sidebar Categories */
+.domainSidebar {
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+/* Apply Docusaurus thin-scrollbar styling */
+.domainSidebar.thin-scrollbar::-webkit-scrollbar {
+ width: 7px;
+}
+
+.domainSidebar.thin-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+ border-radius: 10px;
+}
+
+.domainSidebar.thin-scrollbar::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.2);
+ border-radius: 10px;
+}
+
+.domainSidebar.thin-scrollbar::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+
+[data-theme='dark'] .domainSidebar.thin-scrollbar::-webkit-scrollbar-thumb {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+[data-theme='dark'] .domainSidebar.thin-scrollbar::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(255, 255, 255, 0.3);
+}
+
+.domainSidebarMobile {
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem 0;
+}
+
+/* Remove custom viewport styling - let Docusaurus handle it */
diff --git a/src/theme/DocSidebarItem/Category/index.tsx b/src/theme/DocSidebarItem/Category/index.tsx
new file mode 100644
index 0000000000..590fa86801
--- /dev/null
+++ b/src/theme/DocSidebarItem/Category/index.tsx
@@ -0,0 +1,325 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, {
+ type ComponentProps,
+ type ReactNode,
+ useEffect,
+ useMemo,
+} from 'react';
+import clsx from 'clsx';
+import {
+ ThemeClassNames,
+ useThemeConfig,
+ usePrevious,
+ Collapsible,
+ useCollapsible,
+} from '@docusaurus/theme-common';
+import {isSamePath} from '@docusaurus/theme-common/internal';
+import {
+ isActiveSidebarItem,
+ findFirstSidebarItemLink,
+ useDocSidebarItemsExpandedState,
+ useVisibleSidebarItems,
+} from '@docusaurus/plugin-content-docs/client';
+import Link from '@docusaurus/Link';
+import {translate} from '@docusaurus/Translate';
+import useIsBrowser from '@docusaurus/useIsBrowser';
+import DocSidebarItems from '@theme/DocSidebarItems';
+import DocSidebarItemLink from '@theme/DocSidebarItem/Link';
+import type {Props} from '@theme/DocSidebarItem/Category';
+
+import type {
+ PropSidebarItemCategory,
+ PropSidebarItemLink,
+} from '@docusaurus/plugin-content-docs';
+import DuplicateCounter from '@theme/DocSidebarItem/DuplicateCounter';
+import styles from './styles.module.css';
+
+// If we navigate to a category and it becomes active, it should automatically
+// expand itself
+function useAutoExpandActiveCategory({
+ isActive,
+ collapsed,
+ updateCollapsed,
+ activePath,
+}: {
+ isActive: boolean;
+ collapsed: boolean;
+ updateCollapsed: (b: boolean) => void;
+ activePath: string;
+}) {
+ const wasActive = usePrevious(isActive);
+ const previousActivePath = usePrevious(activePath);
+ useEffect(() => {
+ const justBecameActive = isActive && !wasActive;
+ const stillActiveButPathChanged =
+ isActive && wasActive && activePath !== previousActivePath;
+ if ((justBecameActive || stillActiveButPathChanged) && collapsed) {
+ updateCollapsed(false);
+ }
+ }, [
+ isActive,
+ wasActive,
+ collapsed,
+ updateCollapsed,
+ activePath,
+ previousActivePath,
+ ]);
+}
+
+/**
+ * When a collapsible category has no link, we still link it to its first child
+ * during SSR as a temporary fallback. This allows to be able to navigate inside
+ * the category even when JS fails to load, is delayed or simply disabled
+ * React hydration becomes an optional progressive enhancement
+ * see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
+ * see https://github.com/facebook/docusaurus/issues/3030
+ */
+function useCategoryHrefWithSSRFallback(
+ item: Props['item'],
+): string | undefined {
+ const isBrowser = useIsBrowser();
+ return useMemo(() => {
+ if (item.href && !item.linkUnlisted) {
+ return item.href;
+ }
+ // In these cases, it's not necessary to render a fallback
+ // We skip the "findFirstCategoryLink" computation
+ if (isBrowser || !item.collapsible) {
+ return undefined;
+ }
+ return findFirstSidebarItemLink(item);
+ }, [item, isBrowser]);
+}
+
+function CollapseButton({
+ collapsed,
+ categoryLabel,
+ onClick,
+}: {
+ collapsed: boolean;
+ categoryLabel: string;
+ onClick: ComponentProps<'button'>['onClick'];
+}) {
+ return (
+
+ );
+}
+
+function CategoryLinkLabel({label, duplicateCount}: {label: string; duplicateCount?: number}) {
+ return (
+
+ {label}
+ {duplicateCount && }
+
+ );
+}
+
+export default function DocSidebarItemCategory(props: Props): ReactNode {
+ const visibleChildren = useVisibleSidebarItems(
+ props.item.items,
+ props.activePath,
+ );
+ if (visibleChildren.length === 0) {
+ return ;
+ } else {
+ return ;
+ }
+}
+
+function isCategoryWithHref(
+ category: PropSidebarItemCategory,
+): category is PropSidebarItemCategory & {href: string} {
+ return typeof category.href === 'string';
+}
+
+// If a category doesn't have any visible children, we render it as a link
+function DocSidebarItemCategoryEmpty({item, ...props}: Props): ReactNode {
+ // If the category has no link, we don't render anything
+ // It's not super useful to render a category you can't open nor click
+ if (!isCategoryWithHref(item)) {
+ return null;
+ }
+ // We remove props that don't make sense for a link and forward the rest
+ const {
+ type,
+ collapsed,
+ collapsible,
+ items,
+ linkUnlisted,
+ ...forwardableProps
+ } = item;
+ const linkItem: PropSidebarItemLink = {
+ type: 'link',
+ ...forwardableProps,
+ };
+ return ;
+}
+
+function DocSidebarItemCategoryCollapsible({
+ item,
+ onItemClick,
+ activePath,
+ level,
+ index,
+ ...props
+}: Props): ReactNode {
+ const {items, label, collapsible, className, href} = item;
+ const {
+ docs: {
+ sidebar: {autoCollapseCategories},
+ },
+ } = useThemeConfig();
+ const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);
+ const duplicateCount = item?.customProps?.duplicateCount as number | undefined;
+
+ const isActive = isActiveSidebarItem(item, activePath);
+ const isCurrentPage = isSamePath(href, activePath);
+
+ const {collapsed, setCollapsed} = useCollapsible({
+ // Active categories are always initialized as expanded. The default
+ // (`item.collapsed`) is only used for non-active categories.
+ initialState: () => {
+ if (!collapsible) {
+ return false;
+ }
+ return isActive ? false : item.collapsed;
+ },
+ });
+
+ // Sync collapsed state when item.collapsed prop changes
+ useEffect(() => {
+ if (collapsible && !isActive) {
+ setCollapsed(item.collapsed ?? false);
+ }
+ }, [item.collapsed, collapsible, isActive, setCollapsed]);
+
+ const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();
+ // Use this instead of `setCollapsed`, because it is also reactive
+ const updateCollapsed = (toCollapsed: boolean = !collapsed) => {
+ setExpandedItem(toCollapsed ? null : index);
+ setCollapsed(toCollapsed);
+ };
+ useAutoExpandActiveCategory({
+ isActive,
+ collapsed,
+ updateCollapsed,
+ activePath,
+ });
+ useEffect(() => {
+ if (
+ collapsible &&
+ expandedItem != null &&
+ expandedItem !== index &&
+ autoCollapseCategories
+ ) {
+ setCollapsed(true);
+ }
+ }, [collapsible, expandedItem, index, setCollapsed, autoCollapseCategories]);
+
+ const handleItemClick: ComponentProps<'a'>['onClick'] = (e) => {
+ onItemClick?.(item);
+ if (collapsible) {
+ if (href) {
+ // When already on the category's page, we collapse it
+ // We don't use "isActive" because it would collapse the
+ // category even when we browse a children element
+ // See https://github.com/facebook/docusaurus/issues/11213
+ if (isCurrentPage) {
+ e.preventDefault();
+ updateCollapsed();
+ } else {
+ // When navigating to a new category, we always expand
+ // see https://github.com/facebook/docusaurus/issues/10854#issuecomment-2609616182
+ updateCollapsed(false);
+ }
+ } else {
+ e.preventDefault();
+ updateCollapsed();
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ {href && collapsible && (
+ {
+ e.preventDefault();
+ updateCollapsed();
+ }}
+ />
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/theme/DocSidebarItem/Category/styles.module.css b/src/theme/DocSidebarItem/Category/styles.module.css
new file mode 100644
index 0000000000..622e6ad223
--- /dev/null
+++ b/src/theme/DocSidebarItem/Category/styles.module.css
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+.categoryLink {
+ overflow: hidden;
+}
+
+/*
+TODO merge this logic back in Infima?
+ */
+:global(.menu__link--sublist-caret)::after {
+ margin-left: var(--ifm-menu-link-padding-vertical);
+}
+
+.categoryLinkLabel {
+ flex: 1;
+ overflow: hidden;
+ display: -webkit-box;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ transition: transform 0.3s ease;
+}
diff --git a/src/theme/DocSidebarItem/DuplicateCounter/index.tsx b/src/theme/DocSidebarItem/DuplicateCounter/index.tsx
new file mode 100644
index 0000000000..050080abd9
--- /dev/null
+++ b/src/theme/DocSidebarItem/DuplicateCounter/index.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import styles from './styles.module.css';
+
+interface DuplicateCounterProps {
+ count: number;
+}
+
+export default function DuplicateCounter({ count }: DuplicateCounterProps): React.ReactNode {
+ if (count <= 0) {
+ return null;
+ }
+
+ return (
+
+ [+{count}]
+
+ );
+}
diff --git a/src/theme/DocSidebarItem/DuplicateCounter/styles.module.css b/src/theme/DocSidebarItem/DuplicateCounter/styles.module.css
new file mode 100644
index 0000000000..cb0c8b45a7
--- /dev/null
+++ b/src/theme/DocSidebarItem/DuplicateCounter/styles.module.css
@@ -0,0 +1,8 @@
+.duplicateCounter {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--ifm-color-content-secondary);
+ cursor: pointer;
+ white-space: nowrap;
+ margin-left: 0.25rem;
+}
diff --git a/src/theme/DocSidebarItem/Link/index.tsx b/src/theme/DocSidebarItem/Link/index.tsx
new file mode 100644
index 0000000000..59fda92889
--- /dev/null
+++ b/src/theme/DocSidebarItem/Link/index.tsx
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, {type ReactNode} from 'react';
+import clsx from 'clsx';
+import {ThemeClassNames} from '@docusaurus/theme-common';
+import {isActiveSidebarItem} from '@docusaurus/plugin-content-docs/client';
+import Link from '@docusaurus/Link';
+import isInternalUrl from '@docusaurus/isInternalUrl';
+import IconExternalLink from '@theme/Icon/ExternalLink';
+import DuplicateCounter from '@theme/DocSidebarItem/DuplicateCounter';
+import type {Props} from '@theme/DocSidebarItem/Link';
+
+import styles from './styles.module.css';
+
+function LinkLabel({label, duplicateCount}: {label: string; duplicateCount?: number}) {
+ return (
+
+ {label}
+ {duplicateCount && }
+
+ );
+}
+
+export default function DocSidebarItemLink({
+ item,
+ onItemClick,
+ activePath,
+ level,
+ ...props
+}: Props): ReactNode {
+ const {href, label, className, autoAddBaseUrl} = item;
+ const isActive = isActiveSidebarItem(item, activePath);
+ const isInternalLink = isInternalUrl(href);
+ const duplicateCount = item?.customProps?.duplicateCount as number | undefined;
+
+ return (
+
+ onItemClick(item) : undefined,
+ })}
+ {...props}>
+
+ {!isInternalLink && }
+
+
+ );
+}
diff --git a/src/theme/DocSidebarItem/Link/styles.module.css b/src/theme/DocSidebarItem/Link/styles.module.css
new file mode 100644
index 0000000000..a70e1e8b70
--- /dev/null
+++ b/src/theme/DocSidebarItem/Link/styles.module.css
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+.linkLabel {
+ flex: 1;
+ overflow: hidden;
+ display: -webkit-box;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.menuExternalLink {
+ composes: linkLabel;
+}