From 74a0fcaa62edcfc5304e45d085bec4aaea9f8bec Mon Sep 17 00:00:00 2001 From: Eike Waldt Date: Thu, 16 Apr 2026 09:52:36 +0200 Subject: [PATCH] feat: highlight selected sidebar item and auto-expand it's section Signed-off-by: Eike Waldt On-behalf-of: SAP --- docs/.vitepress/sidebar.ts | 65 +++++++++++++------ .../theme/components/SectionIndex.vue | 38 ++++++++--- docs/.vitepress/theme/style.css | 5 ++ repos-config.json | 8 +-- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index f0d3fec..f3ffb7f 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -4,7 +4,7 @@ import path from 'path'; import matter from 'gray-matter'; export function generateDocumentationSidebar(): any { - const sidebar = generateSidebar({ + const sidebar = generateSidebar([{ documentRootPath: 'docs', scanStartPath: '', resolvePath: '/', @@ -17,26 +17,49 @@ export function generateDocumentationSidebar(): any { sortMenusByFrontmatterOrder: true, frontmatterOrderDefaultValue: 999, prefixSeparator: '/', - }); + }]); // Post-process sidebar to fix folder titles and extract descriptions from frontmatter const fixFolderTitles = (items: any[]): any[] => { return items.map(item => { let frontmatter: Record | undefined; - if (item.link && item.link.endsWith('/')) { - // This is a folder link - read frontmatter from its index.md - const indexPath = path.join('docs', item.link, 'index.md'); - if (fs.existsSync(indexPath)) { - try { - const content = fs.readFileSync(indexPath, 'utf-8'); - frontmatter = matter(content).data; - if (frontmatter?.title) { - item.text = frontmatter.title; - } - } catch (err) { - // Ignore errors, keep the original text + // Fix folder links for proper active state detection + // VitePress normalizes "/tutorials/index.md" to "/tutorials/" (WITH trailing slash) + // The regex /(?:(^|\/)index)?\.(?:md|html)$/ captures the "/" before "index" + // So we must ensure sidebar links for index pages also have trailing slashes + + // IMPORTANT: Don't add leading slash - VitePress adds base path + // If we have "/tutorials/", VitePress makes it "//tutorials/" (external link!) + + if (item.link && item.link.endsWith('/index.md')) { + // Convert "tutorials/index.md" to "tutorials/" + item.link = item.link.replace(/\/index\.md$/, '/'); + } else if (item.link && item.link.startsWith('/')) { + // Remove leading slash that vitepress-sidebar added + item.link = item.link.substring(1); + } + + // Now check if link needs trailing slash + if (item.link && !item.link.endsWith('/')) { + const possibleIndexPath = path.join('docs', item.link, 'index.md'); + if (fs.existsSync(possibleIndexPath)) { + // Add trailing slash: "tutorials" to "tutorials/" + item.link = item.link + '/'; + } + } + + // Check if this is a folder with index.md to read its frontmatter + const possibleIndexPath = path.join('docs', item.link, 'index.md'); + if (item.link && fs.existsSync(possibleIndexPath)) { + try { + const content = fs.readFileSync(possibleIndexPath, 'utf-8'); + frontmatter = matter(content).data; + if (frontmatter?.title) { + item.text = frontmatter.title; } + } catch (err) { + // Ignore errors, keep the original text } } else if (item.link && item.link.endsWith('.md')) { // This is a file link - read frontmatter from the page @@ -76,11 +99,13 @@ export function generateDocumentationSidebar(): any { }); }; - const fixedSidebar = fixFolderTitles(sidebar); + const fixedSidebar: Record = {}; + for (const [key, section] of Object.entries(sidebar)) { + fixedSidebar[key] = { + ...section, + items: fixFolderTitles(section.items), + }; + } - // vitepress-sidebar returns an array, but we need an object with path keys - // Wrap the array result in an object with the root path - return { - '/': fixedSidebar - }; + return fixedSidebar; } diff --git a/docs/.vitepress/theme/components/SectionIndex.vue b/docs/.vitepress/theme/components/SectionIndex.vue index 40d659c..8bda048 100644 --- a/docs/.vitepress/theme/components/SectionIndex.vue +++ b/docs/.vitepress/theme/components/SectionIndex.vue @@ -22,13 +22,17 @@ interface TreeNode { const showDescriptions = computed(() => frontmatter.value.overviewDescriptions !== false); // Get the current directory path from the current page -// e.g., 'how-to/' for how-to/index.md or 'how-to/platform-specific/' for how-to/platform-specific/index.md +// e.g., '/how-to/' for how-to/index.md or '/how-to/platform-specific/' for how-to/platform-specific/index.md const currentDirectory = computed(() => { const path = page.value.relativePath; // Remove /index.md or .md and get directory path const dirPath = path.replace(/\/index\.md$/, "").replace(/\.md$/, ""); - // If it's a directory, add trailing slash - return dirPath ? dirPath + "/" : ""; + // If it's a directory, add trailing slash and leading slash + if (dirPath) { + const normalized = dirPath.startsWith('/') ? dirPath : '/' + dirPath; + return normalized.endsWith('/') ? normalized : normalized + '/'; + } + return "/"; }); // Find items in the current section/subsection from sidebar and build a tree structure @@ -38,7 +42,9 @@ const sectionItems = computed(() => { return []; } - const sidebarGroups = sidebar["/"]; + // Handle both array format and { base, items } format + const sidebarData = sidebar["/"]; + const sidebarGroups = Array.isArray(sidebarData) ? sidebarData : (sidebarData.items || []); const currentPagePath = page.value.relativePath.replace(/\.md$/, ""); const targetPath = currentDirectory.value; @@ -50,8 +56,16 @@ const sectionItems = computed(() => { for (const item of items) { // Check if this item's link matches our target path if (item.link) { - const itemPath = - item.link.replace(/\.md$/, "").replace(/\/index$/, "") + "/"; + // Normalize the link - ensure it starts with / and ends with / + let itemPath = item.link; + if (!itemPath.startsWith('/')) { + itemPath = '/' + itemPath; + } + itemPath = itemPath.replace(/\.md$/, "").replace(/\/index$/, ""); + if (!itemPath.endsWith('/')) { + itemPath = itemPath + '/'; + } + if (itemPath === searchPath) { return item; } @@ -72,8 +86,16 @@ const sectionItems = computed(() => { for (const group of sidebarGroups) { // Check if the group itself matches if (group.link) { - const groupPath = - group.link.replace(/\.md$/, "").replace(/\/index$/, "") + "/"; + // Normalize the link - ensure it starts with / and ends with / + let groupPath = group.link; + if (!groupPath.startsWith('/')) { + groupPath = '/' + groupPath; + } + groupPath = groupPath.replace(/\.md$/, "").replace(/\/index$/, ""); + if (!groupPath.endsWith('/')) { + groupPath = groupPath + '/'; + } + if (groupPath === targetPath) { matchingNode = group; break; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index bc44bc6..46c50ab 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -76,6 +76,11 @@ img[src="/search.png"] { color: var(--vp-c-text-1); } +/* Allow active state to override the text color for sections */ +#VPSidebarNav section.VPSidebarItem.is-active > .item .link > .text { + color: var(--vp-c-brand-1); +} + #VPSidebarNav div.VPSidebarItem > .item .text { font-weight: 500; } diff --git a/repos-config.json b/repos-config.json index d1e62f2..a59ca19 100644 --- a/repos-config.json +++ b/repos-config.json @@ -6,7 +6,7 @@ "docs_path": "docs", "target_path": "projects/gardenlinux", "ref": "docs-ng", - "commit": "7615fe00399f8bb8a6d5b7c36d6a8023747420fa", + "commit": "900bdf88b9e29d97e3a100338fe43be46d333610", "root_files": [ "CONTRIBUTING.md", "SECURITY.md", @@ -33,7 +33,7 @@ "docs_path": "docs", "target_path": "projects/builder", "ref": "docs-ng", - "commit": "ff29b1c4a5abddf855e6b4b7e14bf985a790779e", + "commit": "7a7c3430ba21794d7f30876df8850e1424768149", "media_directories": [ ".media", "assets", @@ -46,7 +46,7 @@ "docs_path": "docs", "target_path": "projects/python-gardenlinux-lib", "ref": "docs-ng", - "commit": "be7fd063d1b440f490d9a1e3054179b35a7abc23", + "commit": "73a16adcc674e43ad9b811c399fa5af1b698d62f", "structure": "sphinx", "media_directories": [ ".media", @@ -73,7 +73,7 @@ "docs_path": "docs", "target_path": "projects/glrd", "ref": "docs-ng", - "commit": "b855783938aad5cf3238fef9b5dc12190e0b7207", + "commit": "b5262e0bf409065ee1b9d62f4d0270695d9081a2", "media_directories": [ ".media", "assets",