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 }) => (