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
8 changes: 4 additions & 4 deletions components/board/BoardCanvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@
transform 0.15s ease,
background-color 0.15s ease;
z-index: 10;
background: radial-gradient(circle, var(--secondary) 30%, transparent 30%);
background: radial-gradient(circle, white 30%, transparent 30%);
}

.connection_handle::before {
Expand All @@ -408,7 +408,7 @@
width: 16px;
height: 16px;
transform: translate(-50%, -50%);
border: 2px solid var(--secondary);
border: 2px solid white;
border-radius: 50%;
opacity: 0.6;
}
Expand All @@ -419,7 +419,7 @@

.connection_handle:hover {
transform: scale(1.2);
background: radial-gradient(circle, var(--secondary) 40%, transparent 40%);
background: radial-gradient(circle, white 40%, transparent 40%);
}

.connection_handle:hover::before {
Expand Down Expand Up @@ -449,6 +449,6 @@

.card_connecting:hover {
box-shadow:
0 0 0 3px var(--secondary),
0 0 0 3px white,
0 4px 16px rgba(0, 0, 0, 0.2);
}
62 changes: 62 additions & 0 deletions components/editor/sidebar/CommentSidebarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { memo, useCallback, useContext } from "react";
import { ProjectContext } from "@src/context/ProjectContext";
import { Comment } from "@src/lib/utils/types";
import { getCommentPositions } from "@src/lib/screenplay/extensions/comment-highlight-extension";
import { focusOnPosition } from "@src/lib/screenplay/editor";
import { join } from "@src/lib/utils/misc";
import nav_item from "./SidebarItem.module.css";

type CommentSidebarItemProps = {
comment: Comment;
isActive: boolean;
onActivate: () => void;
};

const CommentSidebarItem = memo(({ comment, isActive, onActivate }: CommentSidebarItemProps) => {
const { editor } = useContext(ProjectContext);

const handleDoubleClick = useCallback(() => {
if (!editor) return;
const positions = getCommentPositions(editor);
const pos = positions.get(comment.id);
if (pos) {
focusOnPosition(editor, pos.from);
}
}, [comment.id, editor]);

const handleClick = useCallback(() => {
onActivate();
}, [onActivate]);

// Format date similar to other places, keeping it short
const date = new Date(comment.createdAt);
const dateStr = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });

const containerClass = join(
nav_item.container,
isActive ? nav_item.current : "",
);

return (
<div
onClick={handleClick}
onDoubleClick={handleDoubleClick}
className={containerClass}
style={{ cursor: "pointer" }}
>
<div className={nav_item.header}>
<div className={nav_item.title_row}>
<p className={join(nav_item.title, "unselectable")} style={{ fontWeight: 600 }}>{comment.author}</p>
</div>
<span className={nav_item.sceneLength}>{dateStr}</span>
</div>
<p className={join(nav_item.preview, "unselectable")}>{comment.text || "Empty comment"}</p>
</div>
);
});

CommentSidebarItem.displayName = "CommentSidebarItem";

export default CommentSidebarItem;
58 changes: 58 additions & 0 deletions components/editor/sidebar/CommentSidebarView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import { useContext, useMemo } from "react";
import { useTranslations } from "next-intl";
import { ProjectContext } from "@src/context/ProjectContext";
import { SCREENPLAY_EDITOR_CONFIG } from "@src/lib/editor/document-editor-config";
import { useDocumentComments } from "@src/lib/editor/use-document-comments";
import { join } from "@src/lib/utils/misc";
import { MessageSquare } from "lucide-react";
import CommentSidebarItem from "./CommentSidebarItem";

import form from "./../../utils/Form.module.css";
import sidebar_nav from "./EditorSidebarNavigation.module.css";

const CommentSidebarView = () => {
const t = useTranslations("editorSidebar");
const { repository } = useContext(ProjectContext);

const projectState = repository?.getState();
const commentsMap = useMemo(
() =>
projectState && SCREENPLAY_EDITOR_CONFIG.features.comments
? SCREENPLAY_EDITOR_CONFIG.getCommentsMap(projectState)
: null,
[projectState],
);

const { comments, activeCommentId, setActiveCommentId } = useDocumentComments(commentsMap, repository);

const unresolvedComments = useMemo(() => comments.filter((c) => !c.resolved), [comments]);

return (
<>
<div className={sidebar_nav.list_header}>
<MessageSquare size={18} />
<p className={form.label}>{t("comments")}</p>
</div>
<div className={join(sidebar_nav.list, sidebar_nav.scene_list)}>
{unresolvedComments.length !== 0 ? (
unresolvedComments.map((comment) => (
<CommentSidebarItem
key={comment.id}
comment={comment}
isActive={activeCommentId === comment.id}
onActivate={() => setActiveCommentId(comment.id)}
/>
))
) : (
<div className={sidebar_nav.empty_state}>
{t("commentsEmpty")}
</div>
)}
</div>
</>
);
};

export default CommentSidebarView;
12 changes: 12 additions & 0 deletions components/editor/sidebar/EditorSidebarNavigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@
overflow-x: hidden;
}

.empty_state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
color: var(--secondary-text);
font-size: 12px;
}

.scene_list {
flex: 1;
}
Expand Down
23 changes: 18 additions & 5 deletions components/editor/sidebar/EditorSidebarNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { ProjectContext } from "@src/context/ProjectContext";
import { useViewContext } from "@src/context/ViewContext";
import { Scene } from "@src/lib/screenplay/scenes";
import { focusOnPosition } from "@src/lib/screenplay/editor";
import { Archive, Clapperboard } from "lucide-react";
import { Archive, Clapperboard, MessageSquare } from "lucide-react";
import SidebarSceneItem from "./SidebarSceneItem";
import ShelfSidebarView from "./ShelfSidebarView";
import CommentSidebarView from "./CommentSidebarView";

import form from "./../../utils/Form.module.css";
import sidebar_nav from "./EditorSidebarNavigation.module.css";
Expand All @@ -19,7 +20,7 @@ const EditorSidebarNavigation = () => {
const { scenes, updateScenes, editor } = useContext(ProjectContext);
const { leftSidebarOpen } = useViewContext();

const [activeTab, setActiveTab] = useState<"scenes" | "shelf">("scenes");
const [activeTab, setActiveTab] = useState<"scenes" | "shelf" | "comments">("scenes");

const [dragIndex, setDragIndex] = useState<number | null>(null);
// indicatorIndex represents the gap where the item will be inserted.
Expand Down Expand Up @@ -193,7 +194,7 @@ const EditorSidebarNavigation = () => {
className={join(sidebar_nav.list, sidebar_nav.scene_list)}
onPointerMove={handlePointerMove}
>
{scenes.length != 0 &&
{scenes.length != 0 ?
scenes.map((scene: Scene, index: number) => {
const isNoOp =
dragIndex === null ||
Expand All @@ -214,11 +215,17 @@ const EditorSidebarNavigation = () => {
onDoubleClick={handleDoubleClick}
/>
);
})}
}) : (
<div className={sidebar_nav.empty_state}>
{t("scenesEmpty")}
</div>
)}
</div>
</>
) : (
) : activeTab === "shelf" ? (
<ShelfSidebarView />
) : (
<CommentSidebarView />
)}
<div className={sidebar_nav.tab_bar}>
<button
Expand All @@ -227,6 +234,12 @@ const EditorSidebarNavigation = () => {
>
<Clapperboard size={16} />
</button>
<button
className={join(sidebar_nav.tab_btn, activeTab === "comments" ? sidebar_nav.tab_btn_active : "")}
onClick={() => setActiveTab("comments")}
>
<MessageSquare size={16} />
</button>
<button
className={join(sidebar_nav.tab_btn, activeTab === "shelf" ? sidebar_nav.tab_btn_active : "")}
onClick={() => setActiveTab("shelf")}
Expand Down
44 changes: 18 additions & 26 deletions components/editor/sidebar/ShelfSidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,13 @@ const ShelfSidebarItem = memo(({ nodeId, entry, isExpanded, onToggle }: ShelfSid
[nodeId, entry.type, repository, editor],
);

const handleRenameStart = useCallback(
(e: React.MouseEvent, versionId: string, currentTitle: string) => {
e.stopPropagation();
setRenamingVersionId(versionId);
setRenameValue(currentTitle);
// Focus the input on next tick
setTimeout(() => renameInputRef.current?.focus(), 0);
},
[],
);
const handleRenameStart = useCallback((e: React.MouseEvent, versionId: string, currentTitle: string) => {
e.stopPropagation();
setRenamingVersionId(versionId);
setRenameValue(currentTitle);
// Focus the input on next tick
setTimeout(() => renameInputRef.current?.focus(), 0);
}, []);

const handleRenameCommit = useCallback(() => {
if (!renamingVersionId || !repository) return;
Expand All @@ -145,20 +142,13 @@ const ShelfSidebarItem = memo(({ nodeId, entry, isExpanded, onToggle }: ShelfSid
return (
<div className={styles.container}>
<div className={join(styles.header, isExpanded ? styles.header_active : "")} onClick={onToggle}>
<ChevronRight
size={13}
className={join(styles.chevron, isExpanded ? styles.chevron_expanded : "")}
/>
<ChevronRight size={13} className={join(styles.chevron, isExpanded ? styles.chevron_expanded : "")} />
<Icon size={14} className={styles.type_icon} />
<span className={join(styles.title, "unselectable")}>{entry.title}</span>
<span className={join(styles.version_count, "unselectable")}>{entry.versions.length}</span>
<button
className={styles.goto_btn}
title={t("goTo")}
onClick={handleGoTo}
>
<button className={styles.goto_btn} title={t("goTo")} onClick={handleGoTo}>
<CornerDownLeft size={12} />
</button>
<span className={join(styles.version_count, "unselectable")}>{entry.versions.length}</span>
</div>
{isExpanded && (
<div className={styles.versions_list}>
Expand All @@ -185,10 +175,7 @@ const ShelfSidebarItem = memo(({ nodeId, entry, isExpanded, onToggle }: ShelfSid
onClick={(e) => e.stopPropagation()}
/>
) : isConfirmingRestore ? (
<div
className={styles.confirm_row}
onClick={(e) => e.stopPropagation()}
>
<div className={styles.confirm_row} onClick={(e) => e.stopPropagation()}>
<span className={styles.confirm_text}>{t("confirmRestore")}</span>
<div className={styles.confirm_btns}>
<button
Expand All @@ -210,7 +197,9 @@ const ShelfSidebarItem = memo(({ nodeId, entry, isExpanded, onToggle }: ShelfSid
</div>
) : (
<>
<span className={join(styles.version_title, "unselectable")}>{version.title}</span>
<span className={join(styles.version_title, "unselectable")}>
{version.title}
</span>
<div className={styles.version_actions}>
<button
className={styles.action_btn}
Expand All @@ -222,7 +211,10 @@ const ShelfSidebarItem = memo(({ nodeId, entry, isExpanded, onToggle }: ShelfSid
<button
className={styles.action_btn}
title="Restore to screenplay"
onClick={(e) => { e.stopPropagation(); setConfirmRestoreVersionId(version.id); }}
onClick={(e) => {
e.stopPropagation();
setConfirmRestoreVersionId(version.id);
}}
>
<CornerUpLeft size={12} />
</button>
Expand Down
24 changes: 15 additions & 9 deletions components/editor/sidebar/ShelfSidebarView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ const ShelfSidebarView = () => {
<p className={form.label}>{t("shelf")}</p>
</div>
<div className={join(sidebar_nav.list, sidebar_nav.scene_list)}>
{entries.map(([nodeId, entry]) => (
<ShelfSidebarItem
key={nodeId}
nodeId={nodeId}
entry={entry}
isExpanded={expandedId === nodeId}
onToggle={() => setExpandedId(expandedId === nodeId ? null : nodeId)}
/>
))}
{entries.length !== 0 ? (
entries.map(([nodeId, entry]) => (
<ShelfSidebarItem
key={nodeId}
nodeId={nodeId}
entry={entry}
isExpanded={expandedId === nodeId}
onToggle={() => setExpandedId(expandedId === nodeId ? null : nodeId)}
/>
))
) : (
<div className={sidebar_nav.empty_state}>
{t("shelfEmpty")}
</div>
)}
</div>
</>
);
Expand Down
6 changes: 5 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,14 @@
},
"editorSidebar": {
"scenes": "Scenes",
"scenesEmpty": "No scenes yet",
"characters": "Characters",
"locations": "Locations",
"shelf": "Shelf",
"shelfEmpty": "Select a shelved version to edit",
"shelfEmpty": "No shelved items yet",
"comments": "Comments",
"commentsEmpty": "No active comments",
"shelfEmptySelection": "Select a shelved version to edit",
"goTo": "Go to in screenplay",
"confirmRestore": "This will replace the matching content in your screenplay.",
"restore": "Restore",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/screenplay/extensions/placeholder-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const Placeholder = Extension.create<PlaceholderOptions>({
emptyEditorClass: 'is-editor-empty',
emptyNodeClass: 'is-empty',
placeholder: 'Write something …',
showOnlyWhenEditable: true,
showOnlyWhenEditable: false,
showOnlyCurrent: false,
includeChildren: true,
}
Expand Down
Loading