"use client" import { ChevronRight, ChevronDown, ChevronUp, FileText, Folder, FolderOpen } from 'lucide-react' import { cn } from '../../utils/cn' import { DEFAULT_FOLDER_INDEX_FILE as CANONICAL_FOLDER_INDEX_FILE } from '../../utils/doc-tree-nav' export interface NavigationNode { id: string name: string path: string type: 'file' | 'folder' hasReadme?: boolean children?: NavigationNode[] slug?: string // Mirror DocNode's extra optional fields so a DocNode tree is structurally // assignable to NavigationNode[] without an `as` cast. NavigationNode // intentionally never reads these — they're carried for type-compat only. documentType?: 'markdown' | 'pdf' | 'google_sheet' | 'figma' | 'file' sortOrder?: number } interface MultiLevelNavigationProps { nodes: NavigationNode[] selectedPath: string expandedNodes: Set onNodeClick: (node: NavigationNode) => void onToggleExpand?: (nodeId: string) => void isLoading?: boolean className?: string /** Folder-index filename (default `'README.md'`, case-insensitive). When the * selectedPath is a folder with this file, the visual ribbon moves to the * child file. */ folderIndexFile?: string } // SSOT lives in `doc-tree-nav` (canonical case `'README.md'`). The visual // comparator below lowercases both sides, so re-exporting the canonical // constant under the same local name preserves call-site syntax without // drifting from the SSOT. const DEFAULT_FOLDER_INDEX_FILE = CANONICAL_FOLDER_INDEX_FILE /** * Compute the visual "selected" state for a navigation node. * * Folder-with-README convention: when `selectedPath` is a folder that has a * README, the FOLDER doesn't get the ribbon — its README child does. The folder * is just a container; the README is the actual document being viewed. * * Mirror case: a README file whose parent folder is the `selectedPath` gets the * visual selection ribbon (implicit). This handles both the auto-select-first- * folder landing path AND the user-clicks-folder case. */ function isNodeVisuallySelected( node: NavigationNode, selectedPath: string, folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE, ): boolean { const FOLDER_INDEX_FILE = folderIndexFile.toLowerCase() // Path comparisons keyed on `node.path` (raw, includes `.md`). Do NOT use // `node.name` — the tree-builder runs it through `formatDocName` which strips // the `.md` extension, so `node.name.toLowerCase() === 'readme.md'` is always // false. if (selectedPath === node.path) { // Explicit match — but folder-with-README defers to its README child. return !(node.type === 'folder' && node.hasReadme) } if (node.type !== 'file') return false const pathLower = node.path.toLowerCase() // Implicit: README child whose parent folder is the selectedPath. if ( selectedPath !== '' && pathLower === `${selectedPath.toLowerCase()}/${FOLDER_INDEX_FILE}` ) { return true } // Implicit: ROOT README on the landing page (selectedPath === '' means // "no explicit doc selected" — the viewer falls back to the root folder- // index file, so that file IS the active document). if (selectedPath === '' && pathLower === FOLDER_INDEX_FILE) { return true } return false } export function MultiLevelNavigation({ nodes, selectedPath, expandedNodes, onNodeClick, onToggleExpand, isLoading, className, folderIndexFile = DEFAULT_FOLDER_INDEX_FILE, }: MultiLevelNavigationProps) { if (isLoading) { return (
{[1, 2, 3, 4].map((i) => (
))}
) } return (
{nodes.map(node => ( ))}
) } interface NavigationItemProps { node: NavigationNode selectedPath: string expandedNodes: Set onNodeClick: (node: NavigationNode) => void onToggleExpand?: (nodeId: string) => void level: number folderIndexFile: string } function NavigationItem({ node, selectedPath, expandedNodes, onNodeClick, onToggleExpand, level, folderIndexFile, }: NavigationItemProps) { const isExpanded = expandedNodes.has(node.id) const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile) const hasChildren = node.children && node.children.length > 0 return (
0 && "ml-4")}>
{hasChildren && ( )}
{isSelected && (
)}
{hasChildren && isExpanded && (
{node.children!.map(child => ( ))}
)}
) } export function MobileNavigationDropdown({ nodes, selectedPath, expandedNodes, onNodeClick, onToggleExpand, isLoading, className, folderIndexFile = DEFAULT_FOLDER_INDEX_FILE, }: MultiLevelNavigationProps) { if (isLoading) { return (
{[1, 2, 3].map((i) => (
))}
) } return (
{nodes.map(node => ( ))}
) } function MobileNavigationItem({ node, selectedPath, expandedNodes, onNodeClick, onToggleExpand, level, folderIndexFile, }: NavigationItemProps) { const isExpanded = expandedNodes.has(node.id) const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile) const hasChildren = node.children && node.children.length > 0 return (
0 && "ml-3")}>
{hasChildren && ( )}
{isSelected && (
)}
{hasChildren && isExpanded && (
{node.children!.map(child => ( ))}
)}
) }