"use client" /** * OS-style icon folder view for Library — hierarchy, appearance (color + icon), * create (floating sheet drawer like `ExportDrawer` + preview), inline rename, move / delete, and move questions between folders. */ import * as React from "react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Tip } from "@/components/ui/tip" import { cn } from "@/lib/utils" import type { LibraryItem } from "@/lib/mock/library" import { collectFolderDescendantIds, isValidFolderMove, newFolderId, LIBRARY_FOLDER_COLOR_STYLES, LIBRARY_FOLDER_ICON_OPTIONS, type LibraryFolder, type LibraryFolderColorKey, } from "@/lib/mock/library-folders" import { ListPageViewFrame, LIST_PAGE_VIEW_FRAME_MAX_WIDE, } from "@/components/data-views/list-page-view-frame" import { OsFolderGlyph } from "@/components/data-views/os-folder-glyph" import { LibraryNewFolderSheet } from "@/components/library-new-folder-sheet" const COLOR_OPTIONS: LibraryFolderColorKey[] = [ "brand", "success", "warning", "destructive", "muted", "chart1", "chart2", "chart3", ] export interface LibraryOsFolderViewProps { folders: LibraryFolder[] onFoldersChange: React.Dispatch> questions: LibraryItem[] onQuestionsChange: React.Dispatch> } function folderTrail(folders: LibraryFolder[], folderId: string | null): LibraryFolder[] { if (!folderId) return [] const byId = new Map(folders.map(f => [f.id, f])) const trail: LibraryFolder[] = [] let cur: string | null = folderId while (cur) { const f = byId.get(cur) if (!f) break trail.unshift(f) cur = f.parentId } return trail } function folderHoverCounts( folder: LibraryFolder, folders: LibraryFolder[], questions: LibraryItem[], ) { const subfolders = folders.filter(f => f.parentId === folder.id).length const questionsInFolder = questions.filter(q => q.folderId === folder.id).length return { subfolders, questionsInFolder } } function validMoveTargets( folders: LibraryFolder[], movingId: string, ): Array<{ id: string | null; label: string }> { const out: Array<{ id: string | null; label: string }> = [{ id: null, label: "Library (root)" }] for (const f of folders) { if (f.id === movingId) continue if (!isValidFolderMove(folders, movingId, f.id)) continue out.push({ id: f.id, label: f.name }) } return out } export function LibraryOsFolderView({ folders, onFoldersChange, questions, onQuestionsChange, }: LibraryOsFolderViewProps) { const [currentId, setCurrentId] = React.useState(null) const childFolders = React.useMemo( () => folders.filter(f => f.parentId === currentId), [folders, currentId], ) const filesHere = React.useMemo( () => questions.filter(q => q.folderId === currentId), [questions, currentId], ) const trail = React.useMemo(() => folderTrail(folders, currentId), [folders, currentId]) const [createFolderOpen, setCreateFolderOpen] = React.useState(false) const [customizeFolderOpen, setCustomizeFolderOpen] = React.useState(false) const [customizingFolder, setCustomizingFolder] = React.useState(null) const [renamingFolderId, setRenamingFolderId] = React.useState(null) const [renameValue, setRenameValue] = React.useState("") const renameInputRef = React.useRef(null) const [appearanceDialog, setAppearanceDialog] = React.useState(null) const [moveFolderId, setMoveFolderId] = React.useState(null) const [deleteFolderId, setDeleteFolderId] = React.useState(null) React.useEffect(() => { if (renamingFolderId && renameInputRef.current) { renameInputRef.current.focus() renameInputRef.current.select() } }, [renamingFolderId]) function openCreateFolderPanel() { setCreateFolderOpen(true) } function startRename(folder: LibraryFolder) { setRenamingFolderId(folder.id) setRenameValue(folder.name) } function commitRename() { if (!renamingFolderId) return const v = renameValue.trim() const folder = folders.find(f => f.id === renamingFolderId) if (!folder) { setRenamingFolderId(null) return } if (!v) { setRenameValue(folder.name) setRenamingFolderId(null) return } onFoldersChange(prev => prev.map(f => (f.id === renamingFolderId ? { ...f, name: v } : f))) setRenamingFolderId(null) } function cancelRename() { setRenamingFolderId(null) setRenameValue("") } function commitMoveFolder(targetParentId: string | null) { if (!moveFolderId) return if (!isValidFolderMove(folders, moveFolderId, targetParentId)) return onFoldersChange(prev => prev.map(f => (f.id === moveFolderId ? { ...f, parentId: targetParentId } : f)), ) setMoveFolderId(null) } function commitDeleteFolder() { if (!deleteFolderId) return const victim = folders.find(f => f.id === deleteFolderId) if (!victim) return const parent = victim.parentId const desc = collectFolderDescendantIds(folders, deleteFolderId) const remaining = folders.filter(f => !desc.has(f.id)) if (remaining.length === 0) { setDeleteFolderId(null) return } const parentStillExists = parent !== null && remaining.some(f => f.id === parent) const fallbackRoot = remaining.find(f => f.parentId === null)?.id const reassignTarget = parentStillExists ? parent : (fallbackRoot ?? remaining[0]!.id) onFoldersChange(remaining) onQuestionsChange(prev => prev.map(q => (desc.has(q.folderId) ? { ...q, folderId: reassignTarget } : q)), ) if (currentId && desc.has(currentId)) setCurrentId(parentStillExists ? parent : null) setDeleteFolderId(null) } function moveQuestionToFolder(questionId: string, folderId: string) { onQuestionsChange(prev => prev.map(q => (q.id === questionId ? { ...q, folderId } : q)), ) } return ( {/* Breadcrumb navigation */}