diff --git a/src/bach/breadcrumbs.ts b/src/bach/breadcrumbs.ts index a47f6e4..3e2f6cf 100644 --- a/src/bach/breadcrumbs.ts +++ b/src/bach/breadcrumbs.ts @@ -11,7 +11,8 @@ export async function searchPagesForBreadcrumbs( pagePath: string, pages: PageItem[], prefix: { label: string; href: string }[], - titleMap: Map + titleMap: Map, + iconMap: Map ): Promise<{ label: string; href: string }[] | null> { const normalizedTarget = normalizePagePath(pagePath) @@ -40,11 +41,11 @@ export async function searchPagesForBreadcrumbs( return groupPrefix } } else { - const firstChildHref = findFirstHref(buildPages(item.pages, 0, '', titleMap, new Map())) + const firstChildHref = findFirstHref(buildPages(item.pages, 0, '', titleMap, new Map(), iconMap)) groupPrefix.push({ label: item.group, href: firstChildHref ?? '/' }) } - const result = await searchPagesForBreadcrumbs(pagePath, item.pages, groupPrefix, titleMap) + const result = await searchPagesForBreadcrumbs(pagePath, item.pages, groupPrefix, titleMap, iconMap) if (result) return result } } @@ -63,12 +64,13 @@ export async function buildBreadcrumbs( config: DocsConfig, entryId: string, pageTitle: string, - titleMap: Map + titleMap: Map, + iconMap: Map ): Promise<{ label: string; href: string }[]> { const pagePath = normalizeEntryId(entryId) for (const tab of config.navigation.tabs) { - const crumbs = await searchPagesForBreadcrumbs(pagePath, tab.pages, [], titleMap) + const crumbs = await searchPagesForBreadcrumbs(pagePath, tab.pages, [], titleMap, iconMap) if (crumbs) { // Remove the active page itself — breadcrumbs show only the parent path return crumbs.slice(0, -1) diff --git a/src/bach/content.ts b/src/bach/content.ts index 290f1c3..2652d60 100644 --- a/src/bach/content.ts +++ b/src/bach/content.ts @@ -56,6 +56,7 @@ export function buildSidebarEntryMap( id: entry.id, title: entry.data.title, method: 'method' in entry.data && typeof entry.data.method === 'string' ? entry.data.method : undefined, + icon: 'icon' in entry.data && typeof entry.data.icon === 'string' ? entry.data.icon : undefined, sortOrder: 'sortOrder' in entry.data && typeof entry.data.sortOrder === 'number' ? entry.data.sortOrder : undefined, })) @@ -74,6 +75,7 @@ export function buildSidebarEntryMap( export function buildCollectionsSidebarData(allEntries: Map) { const titleMap = new Map() const methodMap = new Map() + const iconMap = new Map() const articles: ArticleEntry[] = [] for (const [, entries] of allEntries) { @@ -86,6 +88,11 @@ export function buildCollectionsSidebarData(allEntries: Map methodMap: Map + iconMap: Map articles: ArticleEntry[] defaultEntriesBySlug: Map } @@ -49,9 +50,9 @@ export class BachSite { const defaultCollection = getDefaultCollection(this._config) const allEntries = await loadCollections(this._config) - const { titleMap, methodMap, articles } = buildCollectionsSidebarData(allEntries) + const { titleMap, methodMap, iconMap, articles } = buildCollectionsSidebarData(allEntries) const collectionsMap = buildSidebarEntryMap(allEntries) - const sidebar = await buildSidebarTree(this._config, titleMap, methodMap, collectionsMap) + const sidebar = await buildSidebarTree(this._config, titleMap, methodMap, iconMap, collectionsMap) const defaultEntriesBySlug = new Map() for (const entry of allEntries.get(defaultCollection) ?? []) { @@ -64,6 +65,7 @@ export class BachSite { sidebar, titleMap, methodMap, + iconMap, articles, defaultEntriesBySlug, } @@ -89,7 +91,13 @@ export class BachSite { if (isApi) { breadcrumbs = [{ label: apiData!.apiLabel }] } else { - breadcrumbs = await buildBreadcrumbs(siteContext.config, entry.id, title, siteContext.titleMap) + breadcrumbs = await buildBreadcrumbs( + siteContext.config, + entry.id, + title, + siteContext.titleMap, + siteContext.iconMap + ) } const { prev, next } = getAdjacentPages(sidebarTree, pathname) diff --git a/src/bach/tree.ts b/src/bach/tree.ts index a4f76f9..1689999 100644 --- a/src/bach/tree.ts +++ b/src/bach/tree.ts @@ -50,6 +50,7 @@ export interface CollectionEntryData { id: string title: string method?: string + icon?: string sortOrder?: number } @@ -67,6 +68,7 @@ export function buildPages( parentPath: string, titleMap: Map, methodMap: Map, + iconMap: Map, collectionsMap?: Map ): SidebarNode[] { const nodes: SidebarNode[] = [] @@ -84,6 +86,7 @@ export function buildPages( href, path: normalizedPath, method: methodMap.get(normalizedPath), + icon: iconMap.get(normalizedPath), } nodes.push(articleNode) } else { @@ -110,12 +113,14 @@ export function buildPages( slug: groupSlug, path: groupPath, href: depth > 0 ? href : undefined, + icon: item.icon, children: sortedEntries.map((entry) => ({ type: 'article', title: entry.title, href: `/${entry.id}`, path: entry.id, method: entry.method, + icon: entry.icon, })), } nodes.push(categoryNode) @@ -136,7 +141,7 @@ export function buildPages( childrenPages = item.pages } - const children = buildPages(childrenPages, depth + 1, groupPath, titleMap, methodMap, collectionsMap) + const children = buildPages(childrenPages, depth + 1, groupPath, titleMap, methodMap, iconMap, collectionsMap) const categoryNode: SidebarCategoryNode = { type: 'category', @@ -144,6 +149,7 @@ export function buildPages( slug: groupSlug, path: groupPath, href: depth > 0 ? href : undefined, + icon: item.icon, children, } nodes.push(categoryNode) @@ -195,9 +201,11 @@ export async function buildSidebarTree( config: DocsConfig, titleMap: Map, methodMap?: Map, + iconMap?: Map, collectionsMap?: Map ): Promise { const _methodMap = methodMap ?? new Map() + const _iconMap = iconMap ?? new Map() const tabs: TabInfo[] = [] const trees: Record = {} @@ -205,7 +213,7 @@ export async function buildSidebarTree( for (const tabItem of config.navigation.tabs) { const tabSlug = slugify(tabItem.tab) - const tabTree = buildPages(tabItem.pages, 0, '', titleMap, _methodMap, collectionsMap) + const tabTree = buildPages(tabItem.pages, 0, '', titleMap, _methodMap, _iconMap, collectionsMap) const firstHref = findFirstHref(tabTree) ?? '/' diff --git a/src/bach/types.ts b/src/bach/types.ts index e37069f..8ea3a26 100644 --- a/src/bach/types.ts +++ b/src/bach/types.ts @@ -17,6 +17,7 @@ export interface SidebarCategoryNode { slug: string path: string href?: string + icon?: string children: SidebarNode[] } @@ -26,6 +27,7 @@ export interface SidebarArticleNode { href: string path: string method?: string + icon?: string } export type SidebarNode = SidebarCategoryNode | SidebarArticleNode @@ -33,12 +35,14 @@ export type SidebarNode = SidebarCategoryNode | SidebarArticleNode export interface GroupItem { group: string root?: string + icon?: string pages: PageItem[] } export interface CollectionGroupItem { group: string root?: string + icon?: string collection: TCollection } diff --git a/src/components/sidebar-tree-view.tsx b/src/components/sidebar-tree-view.tsx index fbf5b10..8c33867 100644 --- a/src/components/sidebar-tree-view.tsx +++ b/src/components/sidebar-tree-view.tsx @@ -1,9 +1,16 @@ import { useState } from 'react' +import { icons } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import { hasActiveChild, isPathActive } from '@/bach/nav' import type { SidebarCategoryNode, SidebarNode } from '@/bach/types' +function TreeIcon({ name }: { name: string }) { + const Icon = icons[name as keyof typeof icons] + if (!Icon) return null + return +} + function SidebarMethodBadge({ method }: { method: string }) { return ( - {node.label} + + {node.icon && } + {node.label} + { e.preventDefault() @@ -79,7 +89,10 @@ function NestedCategory({ onClick={() => setExpanded((v) => !v)} className={`${labelClass} cursor-pointer text-stone-600 hover:bg-black/5 hover:text-stone-900 dark:text-stone-400 dark:hover:bg-white/5 dark:hover:text-stone-100`} > - {node.label} + + {node.icon && } + {node.label} + {chevron} )} @@ -130,12 +143,13 @@ function ChildNode({
  • + {node.icon && } {node.title} {node.method && } @@ -151,8 +165,9 @@ export default function SidebarTreeView({ nodes, currentPath, textSize = 'sm' }: return (

    + {node.icon && } {node.label}