From 39930762555b658b86cc20294b607db21ac5d509 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 24 Aug 2025 12:22:30 -0600 Subject: [PATCH] Implement Discourse Graph Tool and UI Enhancements - Introduced DiscourseGraphPanel component to manage node and relation tools. - Added onEnter cursor behavior for DiscourseGraphTool and relation tools. - Updated Tldraw component to include the new DiscourseGraphTool. - Enhanced UI with new styles for the Discourse Graph tool button. - Refactored UI components to integrate the DiscourseGraphPanel for better tool management. --- .../components/canvas/DiscourseNodeUtil.tsx | 7 + .../DiscourseRelationTool.tsx | 14 +- .../components/canvas/DiscourseToolPanel.tsx | 151 ++++++++++++++++++ apps/roam/src/components/canvas/Tldraw.tsx | 7 + .../src/components/canvas/tldrawStyles.ts | 11 ++ .../src/components/canvas/uiOverrides.tsx | 37 +++-- 6 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 apps/roam/src/components/canvas/DiscourseToolPanel.tsx diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index da1752d9e..f31f90c34 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -103,6 +103,13 @@ export const createNodeShapeTools = ( static initial = "idle"; shapeType = n.type; + override onEnter = () => { + this.editor.setCursor({ + type: "cross", + rotation: 45, + }); + }; + override onPointerDown = () => { const { currentPagePoint } = this.editor.inputs; const shapeId = createShapeId(); diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx index 5256de06e..3e809406b 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationTool.tsx @@ -33,6 +33,12 @@ export const createAllReferencedNodeTools = ( this.Pointing, ]; + override onEnter = () => { + this.editor.setCursor({ + type: "cross", + }); + }; + static Pointing = class extends StateNode { static override id = "pointing"; shape?: DiscourseRelationShape; @@ -268,7 +274,7 @@ export const createAllReferencedNodeTools = ( }; override onEnter = () => { - this.editor.setCursor({ type: "cross", rotation: 0 }); + this.editor.setCursor({ type: "cross" }); }; override onCancel = () => { @@ -314,6 +320,12 @@ export const createAllRelationShapeTools = ( this.Pointing, ]; + override onEnter = () => { + this.editor.setCursor({ + type: "cross", + }); + }; + static Pointing = class extends StateNode { static override id = "pointing"; shape?: DiscourseRelationShape; diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx new file mode 100644 index 000000000..bca827e95 --- /dev/null +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; +import { DiscourseRelation } from "~/utils/getDiscourseRelations"; +import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; +import { useEditor, useValue } from "tldraw"; +import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil"; + +export type DiscourseGraphPanelProps = { + nodes: DiscourseNode[]; + relations: string[]; +}; + +const DiscourseGraphPanel = ({ + nodes, + relations, +}: DiscourseGraphPanelProps) => { + const editor = useEditor(); + + const currentToolId = useValue( + "currentToolId", + () => { + return editor.getCurrentToolId(); + }, + [editor], + ); + + const uniqueRelations = [...new Set(relations)]; + + const currentNodeTool = nodes.find((node) => node.type === currentToolId); + const currentRelationTool = uniqueRelations.find( + (relation) => relation === currentToolId, + ); + + // If it's a node tool, show only that node + if (currentNodeTool) { + return ( +
+
+
editor.setCurrentTool("discourse-tool")} + > + + {currentNodeTool.text} +
+
+
+ ); + } + + // If it's a relation tool, show only that relation + if (currentRelationTool) { + const color = getRelationColor( + currentRelationTool, + uniqueRelations.indexOf(currentRelationTool), + ); + return ( +
+
+
editor.setCurrentTool("discourse-tool")} + > +
+ {currentRelationTool} +
+
+
+ ); + } + + return currentToolId === "discourse-tool" ? ( +
+
+ {/* Nodes Section */} + <> + {nodes.map((n) => ( +
editor.setCurrentTool(n.type)} + > + + {n.text} +
+ ))} + + + {/* Relations Section */} + {uniqueRelations.length > 0 && ( + <> + {uniqueRelations.map((relation, index) => { + const color = getRelationColor(relation, index); + return ( +
editor.setCurrentTool(relation)} + > +
+ {relation} +
+ ); + })} + + )} +
+
+ ) : null; +}; + +export default DiscourseGraphPanel; diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index b89769882..090d10e1e 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import React, { useState, useRef, useMemo, useEffect } from "react"; import ExtensionApiContextProvider, { useExtensionAPI, @@ -39,6 +40,7 @@ import { registerDefaultSideEffects, defaultEditorAssetUrls, usePreloadAssets, + StateNode, } from "tldraw"; import "tldraw/tldraw.css"; import tldrawStyles from "./tldrawStyles"; @@ -317,12 +319,17 @@ const TldrawCanvas = ({ title }: { title: string }) => { ]; // TOOLS + const discourseGraphTool = class DiscourseGraphTool extends StateNode { + static override id = "discourse-tool"; + static override initial = "idle"; + }; const discourseNodeTools = createNodeShapeTools(allNodes); const discourseRelationTools = createAllRelationShapeTools(allRelationNames); const referencedNodeTools = createAllReferencedNodeTools( allAddReferencedNodeByAction, ); const customTools = [ + discourseGraphTool, ...discourseNodeTools, ...discourseRelationTools, ...referencedNodeTools, diff --git a/apps/roam/src/components/canvas/tldrawStyles.ts b/apps/roam/src/components/canvas/tldrawStyles.ts index 468f6304d..b0c78413c 100644 --- a/apps/roam/src/components/canvas/tldrawStyles.ts +++ b/apps/roam/src/components/canvas/tldrawStyles.ts @@ -65,4 +65,15 @@ export default ` white-space: nowrap; font-family: "Inter", sans-serif; } */ + +/* Discourse Graph ToolButton */ + button[data-value="discourse-tool"] div::before { + content: ""; + display: inline-block; + width: 18px; + height: 18px; + background-image: url("data:image/svg+xml,%3Csvg width='256' height='264' viewBox='0 0 256 264' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M156.705 252.012C140.72 267.995 114.803 267.995 98.8183 252.012L11.9887 165.182C-3.99622 149.197 -3.99622 123.28 11.9886 107.296L55.4035 63.8807C63.3959 55.8881 76.3541 55.8881 84.3467 63.8807C92.3391 71.8731 92.3391 84.8313 84.3467 92.8239L69.8751 107.296C53.8901 123.28 53.8901 149.197 69.8751 165.182L113.29 208.596C121.282 216.589 134.241 216.589 142.233 208.596C150.225 200.604 150.225 187.646 142.233 179.653L127.761 165.182C111.777 149.197 111.777 123.28 127.761 107.296C143.746 91.3105 143.746 65.3939 127.761 49.4091L113.29 34.9375C105.297 26.9452 105.297 13.9868 113.29 5.99432C121.282 -1.99811 134.241 -1.99811 142.233 5.99434L243.533 107.296C259.519 123.28 259.519 149.197 243.533 165.182L156.705 252.012ZM200.119 121.767C192.127 113.775 179.168 113.775 171.176 121.767C163.184 129.76 163.184 142.718 171.176 150.71C179.168 158.703 192.127 158.703 200.119 150.71C208.112 142.718 208.112 129.76 200.119 121.767Z' fill='%23000000'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; +} `; diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index 5af4b30ee..6753164fa 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -44,6 +44,7 @@ import { openCanvasDrawer } from "./CanvasDrawer"; import { AddReferencedNodeType } from "./DiscourseRelationShape/DiscourseRelationTool"; import { dispatchToastEvent } from "./ToastListener"; import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil"; +import DiscourseGraphPanel from "./DiscourseToolPanel"; const convertToDiscourseNode = async ({ text, @@ -224,8 +225,13 @@ export const createUiComponents = ({ const tools = useTools(); return ( + - {allNodes.map((n) => ( + {/* {allNodes.map((n) => ( - ))} + ))} */} ); }, @@ -260,13 +266,6 @@ export const createUiComponents = ({ {allNodes.map((n) => ( ))} - {/* {allRelationNames.map((name) => ( - - ))} - {allAddRefNodeActions.map((action) => ( - - ))} - */} @@ -300,6 +299,13 @@ export const createUiComponents = ({ ); }, + SharePanel: () => { + const allRelations = [ + ...allRelationNames, + ...allAddReferencedNodeActions, + ]; + return ; + }, }; }; export const createUiOverrides = ({ @@ -320,6 +326,16 @@ export const createUiOverrides = ({ setConvertToDialogOpen: (open: boolean) => void; }): TLUiOverrides => ({ tools: (editor, tools) => { + tools["discourse-tool"] = { + id: "discourse-tool", + icon: "none", + label: "tool.discourse-tool" as TLUiTranslationKey, + kbd: "", + readonlyOk: true, + onSelect: () => { + editor.setCurrentTool("discourse-tool"); + }, + }; allNodes.forEach((node, index) => { const nodeId = node.type; tools[nodeId] = { @@ -418,7 +434,6 @@ export const createUiOverrides = ({ addToast({ title: "Copied as PNG" }); }, }; - // Disable print keyboard binding to prevent conflict with command palette if (originalPrintAction) { actions["print"] = { @@ -426,7 +441,6 @@ export const createUiOverrides = ({ kbd: "", // Remove keyboard shortcut to prevent conflict }; } - return actions; }, translations: { @@ -442,6 +456,7 @@ export const createUiOverrides = ({ // allAddRefNodeActions.map((name) => [`shape.referenced.${name}`, name]) // ), "action.toggle-full-screen": "Toggle Full Screen", + "tool.discourse-tool": "Discourse Graph", // "action.convert-to": "Convert to", // ...Object.fromEntries( // allNodes.map((node) => [