"use client"
/**
* **Tree view** — `ListPageTreePanelShell` + outline tree + read-only details (`FolderDetailsShell`).
* Hub wiring (folders, mock sheet) stays in the caller; this module hosts library demo wiring only.
*/
import * as React from "react"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Tip } from "@/components/ui/tip"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import {
OutlineTreeCollapsibleContentRail,
OutlineTreeLeafButton,
OutlineTreeMenu,
OutlineTreeMenuItem,
OutlineTreeSub,
OutlineTreeSubItem,
} from "@/components/data-views/outline-tree-menu"
import { ListPageTreePanelShell } from "@/components/data-views/list-page-tree-panel-shell"
import { LIST_PAGE_SPLIT_MILLER_SCROLL_BODY_CLASS } from "@/components/data-views/list-page-split-hub-tokens"
import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
import { LibraryNewFolderSheet } from "@/components/library-new-folder-sheet"
import type {
LibraryItem,
} from "@/lib/mock/library"
import type { LibraryFolder, LibraryFolderColorKey } from "@/lib/mock/library-folders"
import { FolderDetailsShell } from "@/components/folder-details-shell"
import { LibraryQuestionDetailBody } from "@/components/library-question-detail"
// ============================================================================
// TreeItem — recursive folder/question renderer using Collapsible
// ============================================================================
interface TreeItemProps {
folder: LibraryFolder
folders: LibraryFolder[]
questions: LibraryItem[]
selectedItemId: string | null
onSelectItem: (itemId: string) => void
}
function TreeItem({
folder,
folders,
questions,
selectedItemId,
onSelectItem,
}: TreeItemProps) {
const childFolders = folders
.filter(f => f.parentId === folder.id)
.sort((a, b) => a.name.localeCompare(b.name))
const childQuestions = questions
.filter(q => q.folderId === folder.id)
const hasChildren = childFolders.length > 0 || childQuestions.length > 0
const isFolderSelected = selectedItemId === folder.id
return (
{/* Folder row — chevron column + row body (icons align with shadcn tree pattern) */}
{hasChildren ? (
) : (
)}
{hasChildren && (
{childFolders.map(child => (
))}
{childQuestions.map(question => {
const isSelected = selectedItemId === question.id
return (
onSelectItem(question.id)}
aria-selected={isSelected}
role="option"
className="h-auto min-h-8 items-start py-1.5"
>
{question.stem}
{question.questionId}
)
})}
)}
)
}
// ============================================================================
// DetailsPanel — right panel content (folder or question details)
// ============================================================================
interface DetailsPanelProps {
selectedItemId: string | null
folders: LibraryFolder[]
questions: LibraryItem[]
/** Clears tree selection (header dismiss). */
onClearSelection?: () => void
}
function DetailsPanel({ selectedItemId, folders, questions, onClearSelection }: DetailsPanelProps) {
if (!selectedItemId) {
return (
)
}
const folder = folders.find(f => f.id === selectedItemId)
if (folder) {
return (
)
}
const question = questions.find(q => q.id === selectedItemId)
if (question) {
return (
)
}
return (
Item not found
This selection is no longer in the tree. Choose another folder or question.
)
}
// ============================================================================
// HubTreePanelView — tree + details wiring (library demo; reusable shell above)
// ============================================================================
export interface HubTreePanelViewProps {
items: LibraryItem[]
folders: LibraryFolder[]
onItemsChange: (items: LibraryItem[]) => void
onFoldersChange: (folders: LibraryFolder[]) => void
}
export function HubTreePanelView({
items,
folders,
onFoldersChange,
}: HubTreePanelViewProps) {
const [selectedItemId, setSelectedItemId] = React.useState(null)
const [newFolderOpen, setNewFolderOpen] = React.useState(false)
const [newFolderParentId, setNewFolderParentId] = React.useState(null)
const [customizingFolder, setCustomizingFolder] = React.useState(null)
const rootFolders = React.useMemo(
() => folders.filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name)),
[folders],
)
const handleNewFolderCreated = React.useCallback(
(newFolder: { name: string; icon: string; colorKey: LibraryFolderColorKey; parentId: string | null }) => {
if (customizingFolder) {
onFoldersChange(
folders.map(f =>
f.id === customizingFolder.id
? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
: f,
),
)
setCustomizingFolder(null)
} else {
onFoldersChange([
...folders,
{
id: `fld-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
name: newFolder.name,
icon: newFolder.icon,
colorKey: newFolder.colorKey,
parentId: newFolder.parentId,
},
])
}
setNewFolderOpen(false)
},
[customizingFolder, folders, onFoldersChange],
)
return (
<>
Add folder
}
/>
{rootFolders.length === 0 ? (
No folders
) : (
rootFolders.map(folder => (
))
)}
>
}
details={
setSelectedItemId(null)}
/>
}
/>
>
)
}