diff --git a/components/board/BoardCanvas.tsx b/components/board/BoardCanvas.tsx index b0dee47..e7a660e 100644 --- a/components/board/BoardCanvas.tsx +++ b/components/board/BoardCanvas.tsx @@ -35,7 +35,7 @@ interface ArrowContextMenuState { arrow: BoardArrowData; } -const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { +const BoardCanvas = ({ isVisible, docId }: { isVisible: boolean; docId: string }) => { const { repository, isYjsReady, isReadOnly } = useContext(ProjectContext); const t = useTranslations("board"); const projectState = repository?.getState(); @@ -125,7 +125,7 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { useEffect(() => { if (!projectState || !isYjsReady) return; - const boardMap = projectState.board(); + const boardMap = projectState.boardData(docId); const syncCards = () => { const cardsData = boardMap.get("cards"); @@ -176,26 +176,26 @@ const BoardCanvas = ({ isVisible }: { isVisible: boolean }) => { return () => { boardMap.unobserve(syncCards); }; - }, [projectState, isYjsReady, centerCameraOnCards]); + }, [projectState, isYjsReady, docId, centerCameraOnCards]); // Save cards to Yjs const saveCards = useCallback( (newCards: BoardCardData[]) => { if (!projectState || !isYjsReady || isReadOnly) return; - const boardMap = projectState.board(); + const boardMap = projectState.boardData(docId); boardMap.set("cards", JSON.stringify(newCards)); }, - [projectState, isYjsReady, isReadOnly], + [projectState, isYjsReady, isReadOnly, docId], ); // Save arrows to Yjs const saveArrows = useCallback( (newArrows: BoardArrowData[]) => { if (!projectState || !isYjsReady || isReadOnly) return; - const boardMap = projectState.board(); + const boardMap = projectState.boardData(docId); boardMap.set("arrows", JSON.stringify(newArrows)); }, - [projectState, isYjsReady, isReadOnly], + [projectState, isYjsReady, isReadOnly, docId], ); // Handle keyboard events for snapping diff --git a/components/editor/BoardPanel.tsx b/components/editor/BoardPanel.tsx new file mode 100644 index 0000000..0424e93 --- /dev/null +++ b/components/editor/BoardPanel.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useContext } from "react"; +import { useTranslations } from "next-intl"; +import { ProjectContext } from "@src/context/ProjectContext"; +import BoardCanvas from "@components/board/BoardCanvas"; +import { LayoutDashboard } from "lucide-react"; + +import styles from "./EditorPanel.module.css"; + +const EmptyBoardState = () => { + const t = useTranslations("editorSidebar"); + + return ( +
+ +

{t("documentsEmpty")}

+
+ ); +}; + +/** + * Renders the board document currently selected in the document tree. The + * "board" panel is doc-aware: it reads `activeDocument` and mounts a fresh + * BoardCanvas (keyed by id) for the active board, or an empty state when the + * active document isn't a board. + */ +const BoardPanel = ({ isVisible }: { isVisible: boolean }) => { + const { activeDocument } = useContext(ProjectContext); + + if (!activeDocument || activeDocument.type !== "board") { + return ; + } + + return ; +}; + +export default BoardPanel; diff --git a/components/editor/TreeDocumentPanel.tsx b/components/editor/TreeDocumentPanel.tsx new file mode 100644 index 0000000..b014952 --- /dev/null +++ b/components/editor/TreeDocumentPanel.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useContext, useMemo, useCallback } from "react"; +import { useTranslations } from "next-intl"; +import { ProjectContext } from "@src/context/ProjectContext"; +import { createDocumentTreeConfig } from "@src/lib/document-tree/document-tree-config"; +import DocumentEditorPanel from "./DocumentEditorPanel"; +import { FileText } from "lucide-react"; + +import styles from "./EditorPanel.module.css"; + +const EmptyDocumentState = () => { + const t = useTranslations("editorSidebar"); + + return ( +
+ +

{t("documentsEmpty")}

+
+ ); +}; + +const TreeDocumentPanel = ({ isVisible }: { isVisible: boolean }) => { + const { activeDocument, updateDocumentEditor } = useContext(ProjectContext); + + const config = useMemo(() => { + if (!activeDocument || activeDocument.type !== "editor") return null; + return createDocumentTreeConfig(activeDocument.docId); + }, [activeDocument]); + + const handleEditorCreated = useCallback( + (editor: import("@tiptap/react").Editor | null) => { + updateDocumentEditor(editor); + }, + [updateDocumentEditor], + ); + + if (!config || !activeDocument || activeDocument.type !== "editor") { + return ; + } + + return ( + + ); +}; + +export default TreeDocumentPanel; diff --git a/components/editor/sidebar/DocumentTreeItem.module.css b/components/editor/sidebar/DocumentTreeItem.module.css new file mode 100644 index 0000000..58f2e07 --- /dev/null +++ b/components/editor/sidebar/DocumentTreeItem.module.css @@ -0,0 +1,181 @@ +.row { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding-right: 16px; + /* Fixed height so swapping the title for a rename input or a delete-confirm + row never changes the row's size (no layout shift while editing). */ + min-height: 30px; + box-sizing: border-box; + font-size: 12px; + color: var(--secondary-text); + cursor: pointer; +} + +.row:hover { + background-color: var(--editor-sidebar-hover); + color: var(--primary-text); +} + +.row_active { + color: var(--primary-text); + background-color: var(--editor-sidebar-hover); +} + +/* Drop indicators */ +.row_drop_into { + background-color: var(--editor-style-bg-hover); + box-shadow: inset 0 0 0 1px var(--primary-text); +} + +.row_drop_before::before, +.row_drop_after::after { + content: ""; + position: absolute; + left: 8px; + right: 8px; + height: 2px; + background-color: var(--primary-text); + pointer-events: none; +} + +.row_drop_before::before { + top: 0; +} + +.row_drop_after::after { + bottom: 0; +} + +.chevron { + flex-shrink: 0; + color: var(--secondary-text); + transition: transform 0.2s ease; +} + +.chevron_expanded { + transform: rotate(90deg); +} + +.chevron_placeholder { + flex-shrink: 0; + width: 13px; +} + +.type_icon { + flex-shrink: 0; + color: var(--secondary-text); +} + +.title { + flex: 1; + min-width: 0; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.rename_input { + flex: 1; + min-width: 0; + font-size: 12px; + background: var(--editor-sidebar-hover); + border: 1px solid var(--separator); + border-radius: 4px; + padding: 0 6px; + color: var(--primary-text); + outline: none; + line-height: inherit; +} + +.actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.15s; +} + +.row:hover .actions, +.row_active .actions { + opacity: 1; +} + +.action_btn { + display: flex; + align-items: center; + justify-content: center; + padding: 3px; + border-radius: 4px; + border: none; + background: none; + color: var(--secondary-text); + cursor: pointer; + flex-shrink: 0; +} + +.action_btn:hover { + background-color: var(--editor-style-bg-hover); + color: var(--primary-text); +} + +.action_btn_danger:hover { + background-color: var(--error, #e53e3e); + color: #fff; +} + +/* Inline delete confirmation */ +.confirm_row { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} + +.confirm_text { + flex: 1; + min-width: 0; + font-size: 0.72rem; + color: var(--secondary-text); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.confirm_btns { + display: flex; + gap: 6px; + flex-shrink: 0; +} + +.confirm_yes, +.confirm_no { + padding: 3px 10px; + border: none; + border-radius: 5px; + font-size: 0.72rem; + font-weight: 500; + cursor: pointer; + transition: opacity 0.1s ease; +} + +.confirm_yes { + background-color: var(--error, #e53e3e); + color: #fff; +} + +.confirm_no { + background-color: var(--tertiary); + color: var(--primary-text); +} + +.confirm_yes:hover, +.confirm_no:hover { + opacity: 0.85; +} diff --git a/components/editor/sidebar/DocumentTreeItem.tsx b/components/editor/sidebar/DocumentTreeItem.tsx new file mode 100644 index 0000000..35ba2cc --- /dev/null +++ b/components/editor/sidebar/DocumentTreeItem.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; +import { DocumentNode, DocumentNodeType } from "@src/lib/project/project-state"; +import { join } from "@src/lib/utils/misc"; +import { + ChevronRight, + FilePlus, + FileText, + Folder, + FolderPlus, + LayoutDashboard, + Pencil, + Trash2, +} from "lucide-react"; + +import styles from "./DocumentTreeItem.module.css"; + +const TYPE_ICONS: Record = { + folder: Folder, + editor: FileText, + board: LayoutDashboard, +}; + +export type DropPosition = "into" | "before" | "after"; + +export interface DocumentTreeItemProps { + node: DocumentNode; + depth: number; + childrenOf: (parentId: string) => DocumentNode[]; + expanded: Set; + onToggle: (id: string) => void; + activeDocId: string | null; + onOpen: (node: DocumentNode) => void; + onCreateChild: (parentId: string, type: "folder" | "editor") => void; + onRename: (id: string, title: string) => void; + onDelete: (id: string) => void; + // Drag & drop + draggingId: string | null; + dropTarget: { id: string; pos: DropPosition } | null; + onDragStart: (id: string) => void; + onDragOverNode: (id: string, pos: DropPosition) => void; + onDropNode: (id: string) => void; + onDragEnd: () => void; +} + +const INDENT = 14; + +const DocumentTreeItem = ({ + node, + depth, + childrenOf, + expanded, + onToggle, + activeDocId, + onOpen, + onCreateChild, + onRename, + onDelete, + draggingId, + dropTarget, + onDragStart, + onDragOverNode, + onDropNode, + onDragEnd, +}: DocumentTreeItemProps) => { + const t = useTranslations("editorSidebar"); + const [isRenaming, setIsRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(""); + const [confirmingDelete, setConfirmingDelete] = useState(false); + const renameInputRef = useRef(null); + + const Icon = TYPE_ICONS[node.type]; + const isFolder = node.type === "folder"; + const isOpen = expanded.has(node.id); + const isActive = node.type === "editor" && activeDocId === node.id; + const busy = isRenaming || confirmingDelete; + + const handleRowClick = useCallback(() => { + if (busy) return; + if (isFolder) onToggle(node.id); + else onOpen(node); + }, [busy, isFolder, node, onToggle, onOpen]); + + const startRename = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setRenameValue(node.title); + setIsRenaming(true); + setTimeout(() => renameInputRef.current?.select(), 0); + }, + [node.title], + ); + + const commitRename = useCallback(() => { + const trimmed = renameValue.trim(); + if (trimmed) onRename(node.id, trimmed); + setIsRenaming(false); + }, [renameValue, node.id, onRename]); + + const handleDragOver = useCallback( + (e: React.DragEvent) => { + if (!draggingId || draggingId === node.id) return; + e.preventDefault(); + e.stopPropagation(); + const rect = e.currentTarget.getBoundingClientRect(); + const y = e.clientY - rect.top; + let pos: DropPosition; + if (isFolder) { + if (y < rect.height * 0.25) pos = "before"; + else if (y > rect.height * 0.75) pos = "after"; + else pos = "into"; + } else { + pos = y < rect.height / 2 ? "before" : "after"; + } + onDragOverNode(node.id, pos); + }, + [draggingId, node.id, isFolder, onDragOverNode], + ); + + const isDropTarget = dropTarget?.id === node.id; + const rowClass = join( + styles.row, + isActive ? styles.row_active : "", + isDropTarget && dropTarget?.pos === "into" ? styles.row_drop_into : "", + isDropTarget && dropTarget?.pos === "before" ? styles.row_drop_before : "", + isDropTarget && dropTarget?.pos === "after" ? styles.row_drop_after : "", + ); + + return ( + <> +
{ + e.dataTransfer.effectAllowed = "move"; + onDragStart(node.id); + }} + onDragOver={handleDragOver} + onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); + onDropNode(node.id); + }} + onDragEnd={onDragEnd} + > + {isFolder ? ( + + ) : ( + + )} + + + {isRenaming ? ( + setRenameValue(e.target.value)} + onBlur={commitRename} + onMouseDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === "Enter") commitRename(); + else if (e.key === "Escape") setIsRenaming(false); + }} + /> + ) : confirmingDelete ? ( +
e.stopPropagation()}> + + {isFolder ? t("confirmDeleteFolder") : t("confirmDelete")} + +
+ + +
+
+ ) : ( + <> + {node.title} +
+ {isFolder && ( + <> + + + + )} + + +
+ + )} +
+ + {isFolder && + isOpen && + childrenOf(node.id).map((child) => ( + + ))} + + ); +}; + +export default DocumentTreeItem; diff --git a/components/editor/sidebar/DocumentTreeSidebarView.tsx b/components/editor/sidebar/DocumentTreeSidebarView.tsx new file mode 100644 index 0000000..9da6ed0 --- /dev/null +++ b/components/editor/sidebar/DocumentTreeSidebarView.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { useCallback, useContext, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; +import { ProjectContext } from "@src/context/ProjectContext"; +import { useViewContext } from "@src/context/ViewContext"; +import { DocumentNode } from "@src/lib/project/project-state"; +import { join } from "@src/lib/utils/misc"; +import { FilePlus, FolderPlus, FolderTree, LayoutDashboard } from "lucide-react"; +import DocumentTreeItem, { DropPosition } from "./DocumentTreeItem"; + +import form from "./../../utils/Form.module.css"; +import sidebar_nav from "./EditorSidebarNavigation.module.css"; + +const DocumentTreeSidebarView = () => { + const t = useTranslations("editorSidebar"); + const { documents, repository, activeDocument, setActiveDocument } = useContext(ProjectContext); + const { setSecondaryPanel } = useViewContext(); + + const [expanded, setExpanded] = useState>(new Set()); + const [draggingId, setDraggingId] = useState(null); + const [dropTarget, setDropTarget] = useState<{ id: string; pos: DropPosition } | null>(null); + const [rootDrop, setRootDrop] = useState(false); + + // Children of a parent (null = root), sorted by fractional `order`. + const childrenOf = useCallback( + (parentId: string | null): DocumentNode[] => + Object.values(documents) + .filter((n) => (n.parentId ?? null) === (parentId ?? null)) + .sort((a, b) => a.order - b.order), + [documents], + ); + + const roots = useMemo(() => childrenOf(null), [childrenOf]); + + const appendOrder = useCallback( + (parentId: string | null, excludeId?: string) => { + const kids = childrenOf(parentId).filter((n) => n.id !== excludeId); + return kids.length ? kids[kids.length - 1].order + 1 : 0; + }, + [childrenOf], + ); + + const toggle = useCallback((id: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + + const openDocument = useCallback( + (node: DocumentNode) => { + if (node.type === "board") { + setActiveDocument({ docId: node.id, type: "board" }); + setSecondaryPanel("board"); + } else if (node.type === "editor") { + setActiveDocument({ docId: node.id, type: "editor" }); + setSecondaryPanel("document"); + } + }, + [setActiveDocument, setSecondaryPanel], + ); + + const createChild = useCallback( + (parentId: string | null, type: "folder" | "editor") => { + if (!repository) return; + if (type === "folder") repository.createFolder(t("untitledFolder"), parentId); + else repository.createEditorDocument(t("untitledDocument"), parentId); + }, + [repository, t], + ); + + const createBoard = useCallback(() => { + repository?.createBoardDocument(t("boardTitle"), null); + }, [repository, t]); + + const renameDocument = useCallback( + (id: string, title: string) => repository?.renameDocument(id, title), + [repository], + ); + + const deleteDocument = useCallback( + (id: string) => { + if (!repository) return; + // Clear the open document if it (or one of its descendants) is being removed. + const removed = new Set(); + const stack = [id]; + while (stack.length) { + const cur = stack.pop()!; + removed.add(cur); + for (const n of Object.values(documents)) if (n.parentId === cur) stack.push(n.id); + } + if (activeDocument && removed.has(activeDocument.docId)) setActiveDocument(null); + repository.deleteDocument(id); + }, + [repository, documents, activeDocument, setActiveDocument], + ); + + // ---- Drag & drop ---- + const onDragStart = useCallback((id: string) => setDraggingId(id), []); + + const onDragOverNode = useCallback( + (id: string, pos: DropPosition) => { + setRootDrop(false); + setDropTarget((prev) => (prev?.id === id && prev.pos === pos ? prev : { id, pos })); + }, + [], + ); + + const resetDrag = useCallback(() => { + setDraggingId(null); + setDropTarget(null); + setRootDrop(false); + }, []); + + const onDropNode = useCallback( + (targetId: string) => { + const dragId = draggingId; + const target = documents[targetId]; + const pos = dropTarget?.pos; + resetDrag(); + if (!repository || !dragId || !target || !pos || dragId === targetId) return; + + if (pos === "into") { + repository.moveDocument(dragId, target.id, appendOrder(target.id, dragId)); + setExpanded((prev) => new Set(prev).add(target.id)); + return; + } + + const parentId = target.parentId ?? null; + const siblings = childrenOf(parentId).filter((n) => n.id !== dragId); + const idx = siblings.findIndex((n) => n.id === targetId); + let order: number; + if (pos === "before") { + const prev = siblings[idx - 1]; + order = prev ? (prev.order + target.order) / 2 : target.order - 1; + } else { + const next = siblings[idx + 1]; + order = next ? (target.order + next.order) / 2 : target.order + 1; + } + repository.moveDocument(dragId, parentId, order); + }, + [draggingId, documents, dropTarget, repository, appendOrder, childrenOf, resetDrag], + ); + + const onDropRoot = useCallback(() => { + const dragId = draggingId; + resetDrag(); + if (!repository || !dragId) return; + repository.moveDocument(dragId, null, appendOrder(null, dragId)); + }, [draggingId, repository, appendOrder, resetDrag]); + + return ( + <> +
+ +

{t("documents")}

+
+
+ + + +
+
+
{ + if (!draggingId) return; + e.preventDefault(); + setDropTarget(null); + setRootDrop(true); + }} + onDrop={(e) => { + e.preventDefault(); + onDropRoot(); + }} + > + {roots.length !== 0 ? ( + roots.map((node) => ( + + )) + ) : ( +
{t("documentsEmpty")}
+ )} +
+ + ); +}; + +export default DocumentTreeSidebarView; diff --git a/components/editor/sidebar/EditorSidebarNavigation.module.css b/components/editor/sidebar/EditorSidebarNavigation.module.css index 6587655..bf02034 100644 --- a/components/editor/sidebar/EditorSidebarNavigation.module.css +++ b/components/editor/sidebar/EditorSidebarNavigation.module.css @@ -82,6 +82,45 @@ font-size: 1rem; } +/* Header action buttons (e.g. document-tree create buttons) */ +.header_spacer { + flex: 1; +} + +.header_actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 2px; + padding-right: 16px; +} + +.header_btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 6px; + border: none; + background: none; + color: var(--secondary-text); + cursor: pointer; + transition: + color 0.15s, + background-color 0.15s; +} + +.header_btn:hover { + background-color: var(--editor-sidebar-hover); + color: var(--primary-text); +} + +/* Drop-to-root target highlight */ +.tree_root_drop { + box-shadow: inset 0 0 0 1px var(--primary-text); + border-radius: 8px; +} + .list_fill { min-height: 30px; height: 100%; @@ -91,25 +130,27 @@ .tab_bar { display: flex; flex-direction: row; + justify-content: center; border-top: 1px solid var(--separator); margin-top: auto; padding-top: 4px; padding-inline: 12px; - gap: 4px; + gap: 8px; } .tab_btn { - flex: 1; display: flex; align-items: center; justify-content: center; - padding: 8px 0; + padding: 8px 12px; border-radius: 8px; cursor: pointer; color: var(--secondary-text); background: none; border: none; - transition: color 0.15s, background-color 0.15s; + transition: + color 0.15s, + background-color 0.15s; } .tab_btn:hover { @@ -119,4 +160,9 @@ .tab_btn_active { color: var(--primary-text); + background-color: var(--editor-sidebar-hover); +} + +.tab_btn_active:hover { + color: var(--primary-text); } diff --git a/components/editor/sidebar/EditorSidebarNavigation.tsx b/components/editor/sidebar/EditorSidebarNavigation.tsx index dae8d2d..a4f5d42 100644 --- a/components/editor/sidebar/EditorSidebarNavigation.tsx +++ b/components/editor/sidebar/EditorSidebarNavigation.tsx @@ -8,10 +8,11 @@ import { useViewContext } from "@src/context/ViewContext"; import { Scene } from "@src/lib/screenplay/scenes"; import { focusOnPosition } from "@src/lib/screenplay/editor"; import { computeSceneLabels } from "@src/lib/screenplay/scene-locking"; -import { Archive, Clapperboard, MessageSquare } from "lucide-react"; +import { Archive, Clapperboard, FolderTree, MessageSquare } from "lucide-react"; import SidebarSceneItem from "./SidebarSceneItem"; import ShelfSidebarView from "./ShelfSidebarView"; import CommentSidebarView from "./CommentSidebarView"; +import DocumentTreeSidebarView from "./DocumentTreeSidebarView"; import form from "./../../utils/Form.module.css"; import sidebar_nav from "./EditorSidebarNavigation.module.css"; @@ -29,7 +30,7 @@ const EditorSidebarNavigation = () => { } = useContext(ProjectContext); const { leftSidebarOpen } = useViewContext(); - const [activeTab, setActiveTab] = useState<"scenes" | "shelf" | "comments">("scenes"); + const [activeTab, setActiveTab] = useState<"scenes" | "shelf" | "comments" | "documents">("scenes"); const [dragIndex, setDragIndex] = useState(null); // indicatorIndex represents the gap where the item will be inserted. @@ -256,6 +257,8 @@ const EditorSidebarNavigation = () => { ) : activeTab === "shelf" ? ( + ) : activeTab === "documents" ? ( + ) : ( )} @@ -266,6 +269,12 @@ const EditorSidebarNavigation = () => { > + +
)} diff --git a/components/project/SplitPanelContainer.tsx b/components/project/SplitPanelContainer.tsx index 33776f4..87611b6 100644 --- a/components/project/SplitPanelContainer.tsx +++ b/components/project/SplitPanelContainer.tsx @@ -7,17 +7,18 @@ import { PanelType, useViewContext } from "@src/context/ViewContext"; import EditorPanel from "@components/editor/EditorPanel"; import TitlePagePanel from "@components/editor/TitlePagePanel"; import DraftEditorPanel from "@components/editor/DraftEditorPanel"; -import BoardCanvas from "@components/board/BoardCanvas"; +import TreeDocumentPanel from "@components/editor/TreeDocumentPanel"; +import BoardPanel from "@components/editor/BoardPanel"; import StatisticsClientPage from "@components/projects/stats/StatisticsClientPage"; import DragHandle from "./DragHandle"; import { SuggestionData } from "@components/editor/SuggestionMenu"; import { Archive, + ArrowLeftRight, ChevronLeft, ChevronRight, Clapperboard, FileText, - LayoutDashboard, Maximize, Menu, MessageSquare, @@ -57,19 +58,22 @@ const PanelRenderer = ({ /> ); case "board": - return ; + return ; case "statistics": return ; case "title": return ; case "draft": return ; + case "document": + return ; } }; +// Boards and tree documents are opened from the document-tree sidebar (they are +// per-document), so they are not listed here. const SWITCHABLE_PANELS: { type: PanelType; icon: typeof Clapperboard; labelKey: string }[] = [ { type: "screenplay", icon: Clapperboard, labelKey: "screenplay" }, - { type: "board", icon: LayoutDashboard, labelKey: "board" }, { type: "title", icon: FileText, labelKey: "titlePage" }, { type: "draft", icon: Archive, labelKey: "draftEditor" }, ]; @@ -82,6 +86,7 @@ const PanelSwitcherMenu = ({ currentPanel, side }: { currentPanel: PanelType; si isSplit, primaryPanel, setSecondaryPanel, + swapPanels, isEndlessScroll, setIsEndlessScroll, showComments, @@ -177,6 +182,17 @@ const PanelSwitcherMenu = ({ currentPanel, side }: { currentPanel: PanelType; si {isSplit ? : } {isSplit ? t("unsplitPanel") : t("splitPanel")} +
{SWITCHABLE_PANELS.map(({ type, icon: Icon, labelKey }) => (