Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions components/board/BoardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions components/editor/BoardPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.editor_panel} style={{ alignItems: "center", justifyContent: "center" }}>
<LayoutDashboard size={32} style={{ opacity: 0.3, marginBottom: 12 }} />
<p style={{ opacity: 0.5, fontSize: 13 }}>{t("documentsEmpty")}</p>
</div>
);
};

/**
* 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 <EmptyBoardState />;
}

return <BoardCanvas key={activeDocument.docId} docId={activeDocument.docId} isVisible={isVisible} />;
};

export default BoardPanel;
53 changes: 53 additions & 0 deletions components/editor/TreeDocumentPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.editor_panel} style={{ alignItems: "center", justifyContent: "center" }}>
<FileText size={32} style={{ opacity: 0.3, marginBottom: 12 }} />
<p style={{ opacity: 0.5, fontSize: 13 }}>{t("documentsEmpty")}</p>
</div>
);
};

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 <EmptyDocumentState />;
}

return (
<DocumentEditorPanel
key={activeDocument.docId}
config={config}
isVisible={isVisible}
onEditorCreated={handleEditorCreated}
focusedTypeOverride="draft"
/>
);
};

export default TreeDocumentPanel;
181 changes: 181 additions & 0 deletions components/editor/sidebar/DocumentTreeItem.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading