Skip to content
Merged
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
12 changes: 7 additions & 5 deletions src/bach/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export async function searchPagesForBreadcrumbs<TCollection extends string>(
pagePath: string,
pages: PageItem<TCollection>[],
prefix: { label: string; href: string }[],
titleMap: Map<string, string>
titleMap: Map<string, string>,
iconMap: Map<string, string>
): Promise<{ label: string; href: string }[] | null> {
const normalizedTarget = normalizePagePath(pagePath)

Expand Down Expand Up @@ -40,11 +41,11 @@ export async function searchPagesForBreadcrumbs<TCollection extends string>(
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
}
}
Expand All @@ -63,12 +64,13 @@ export async function buildBreadcrumbs<TCollection extends string>(
config: DocsConfig<TCollection>,
entryId: string,
pageTitle: string,
titleMap: Map<string, string>
titleMap: Map<string, string>,
iconMap: Map<string, string>
): 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)
Expand Down
9 changes: 8 additions & 1 deletion src/bach/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))
Expand All @@ -74,6 +75,7 @@ export function buildSidebarEntryMap(
export function buildCollectionsSidebarData(allEntries: Map<string, DynamicCollectionEntry[]>) {
const titleMap = new Map<string, string>()
const methodMap = new Map<string, string>()
const iconMap = new Map<string, string>()
const articles: ArticleEntry[] = []

for (const [, entries] of allEntries) {
Expand All @@ -86,6 +88,11 @@ export function buildCollectionsSidebarData(allEntries: Map<string, DynamicColle
methodMap.set(entry.id, method)
methodMap.set(slug, method)
}
const icon = 'icon' in entry.data && typeof entry.data.icon === 'string' ? entry.data.icon : undefined
if (icon) {
iconMap.set(entry.id, icon)
iconMap.set(slug, icon)
}
articles.push({
slug,
title: entry.data.title,
Expand All @@ -94,7 +101,7 @@ export function buildCollectionsSidebarData(allEntries: Map<string, DynamicColle
}
}

return { titleMap, methodMap, articles }
return { titleMap, methodMap, iconMap, articles }
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/bach/schemas/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'astro/zod'
export const docsSchema = z.object({
title: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
prose: z.boolean().default(true),
})

Expand Down
14 changes: 11 additions & 3 deletions src/bach/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface SiteContext {
sidebar: SidebarTreeResult
titleMap: Map<string, string>
methodMap: Map<string, string>
iconMap: Map<string, string>
articles: ArticleEntry[]
defaultEntriesBySlug: Map<string, DynamicCollectionEntry>
}
Expand Down Expand Up @@ -49,9 +50,9 @@ export class BachSite<TCollection extends string = string> {

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<string, DynamicCollectionEntry>()
for (const entry of allEntries.get(defaultCollection) ?? []) {
Expand All @@ -64,6 +65,7 @@ export class BachSite<TCollection extends string = string> {
sidebar,
titleMap,
methodMap,
iconMap,
articles,
defaultEntriesBySlug,
}
Expand All @@ -89,7 +91,13 @@ export class BachSite<TCollection extends string = string> {
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)
Expand Down
12 changes: 10 additions & 2 deletions src/bach/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface CollectionEntryData {
id: string
title: string
method?: string
icon?: string
sortOrder?: number
}

Expand All @@ -67,6 +68,7 @@ export function buildPages<TCollection extends string>(
parentPath: string,
titleMap: Map<string, string>,
methodMap: Map<string, string>,
iconMap: Map<string, string>,
collectionsMap?: Map<TCollection, CollectionEntryData[]>
): SidebarNode[] {
const nodes: SidebarNode[] = []
Expand All @@ -84,6 +86,7 @@ export function buildPages<TCollection extends string>(
href,
path: normalizedPath,
method: methodMap.get(normalizedPath),
icon: iconMap.get(normalizedPath),
}
nodes.push(articleNode)
} else {
Expand All @@ -110,12 +113,14 @@ export function buildPages<TCollection extends string>(
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)
Expand All @@ -136,14 +141,15 @@ export function buildPages<TCollection extends string>(
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',
label: item.group,
slug: groupSlug,
path: groupPath,
href: depth > 0 ? href : undefined,
icon: item.icon,
children,
}
nodes.push(categoryNode)
Expand Down Expand Up @@ -195,17 +201,19 @@ export async function buildSidebarTree<TCollection extends string>(
config: DocsConfig<TCollection>,
titleMap: Map<string, string>,
methodMap?: Map<string, string>,
iconMap?: Map<string, string>,
collectionsMap?: Map<TCollection, CollectionEntryData[]>
): Promise<SidebarTreeResult> {
const _methodMap = methodMap ?? new Map()
const _iconMap = iconMap ?? new Map()

const tabs: TabInfo[] = []
const trees: Record<string, SidebarNode[]> = {}
const slugToTab: Record<string, string> = {}

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) ?? '/'

Expand Down
4 changes: 4 additions & 0 deletions src/bach/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface SidebarCategoryNode {
slug: string
path: string
href?: string
icon?: string
children: SidebarNode[]
}

Expand All @@ -26,19 +27,22 @@ export interface SidebarArticleNode {
href: string
path: string
method?: string
icon?: string
}

export type SidebarNode = SidebarCategoryNode | SidebarArticleNode

export interface GroupItem<TCollection extends string = string> {
group: string
root?: string
icon?: string
pages: PageItem<TCollection>[]
}

export interface CollectionGroupItem<TCollection extends string = string> {
group: string
root?: string
icon?: string
collection: TCollection
}

Expand Down
23 changes: 19 additions & 4 deletions src/components/sidebar-tree-view.tsx
Original file line number Diff line number Diff line change
@@ -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 <Icon className="h-4 w-4 shrink-0" />
Comment on lines +2 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Entire icon library bundled due to dynamic key lookup

import { icons } from 'lucide-react' pulls in the entire icon map (~1400+ components). Because the lookup is a runtime string key (icons[name as keyof typeof icons]), bundlers like Vite/Rollup cannot tree-shake unused icons — every icon ships to the client even if only two or three are ever referenced. For a documentation site sidebar this could add hundreds of KB to the JS bundle. A common alternative is to use a lazy-load registry or restrict the allowed icon set to named imports that the bundler can prune.

}

function SidebarMethodBadge({ method }: { method: string }) {
return (
<Badge
Expand Down Expand Up @@ -62,7 +69,10 @@ function NestedCategory({
: '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'
}`}
>
<span>{node.label}</span>
<span className="flex items-center gap-1.5">
{node.icon && <TreeIcon name={node.icon} />}
{node.label}
</span>
<span
onClick={(e) => {
e.preventDefault()
Expand All @@ -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`}
>
<span>{node.label}</span>
<span className="flex items-center gap-1.5">
{node.icon && <TreeIcon name={node.icon} />}
{node.label}
</span>
{chevron}
</button>
)}
Expand Down Expand Up @@ -130,12 +143,13 @@ function ChildNode({
<li>
<a
href={node.href}
className={`flex items-center rounded-md ${nested ? 'pl-5' : 'pl-2'} pr-2 py-1.5 ${textSize === 'base' ? 'text-base' : 'text-sm'} transition-colors ${
className={`flex items-center gap-1.5 rounded-md ${nested ? 'pl-5' : 'pl-2'} pr-2 py-1.5 ${textSize === 'base' ? 'text-base' : 'text-sm'} transition-colors ${
active
? `text-primary bg-primary/10 dark:bg-primary/15 font-medium${nested ? ' relative before:absolute before:left-2.5 before:top-2 before:bottom-2 before:w-px before:bg-primary' : ''}`
: '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.icon && <TreeIcon name={node.icon} />}
<span className="min-w-0 truncate">{node.title}</span>
{node.method && <SidebarMethodBadge method={node.method} />}
</a>
Expand All @@ -151,8 +165,9 @@ export default function SidebarTreeView({ nodes, currentPath, textSize = 'sm' }:
return (
<div key={node.path} className="mb-8">
<h3
className={`mb-[.625rem] pl-2 pr-2 ${textSize === 'base' ? 'text-base' : 'text-sm'} font-semibold text-[rgb(22,27,30)] dark:text-[rgb(222,226,230)]`}
className={`mb-[.625rem] flex items-center gap-1.5 pl-2 pr-2 ${textSize === 'base' ? 'text-base' : 'text-sm'} font-semibold text-[rgb(22,27,30)] dark:text-[rgb(222,226,230)]`}
>
{node.icon && <TreeIcon name={node.icon} />}
{node.label}
</h3>
<ul className="space-y-0.5">
Expand Down
Loading