From 36a3a28944e2ed21d52bf686727122535e781524 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 1 Dec 2025 12:03:52 -0600 Subject: [PATCH 1/4] Refactor color retrieval in BaseDiscourseNodeUtil: replace manual color logic with getDiscourseNodeColors utility function for improved maintainability and clarity. --- .../components/canvas/DiscourseNodeUtil.tsx | 26 ++---------- apps/roam/src/utils/getDiscourseNodeColors.ts | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 apps/roam/src/utils/getDiscourseNodeColors.ts diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index 0c4150b79..90ca43954 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -33,7 +33,6 @@ import createDiscourseNode from "~/utils/createDiscourseNode"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import { isPageUid } from "./Tldraw"; import LabelDialog from "./LabelDialog"; -import { colord } from "colord"; import { discourseContext } from "./Tldraw"; import getDiscourseContextResults from "~/utils/getDiscourseContextResults"; import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; @@ -46,7 +45,7 @@ import { } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; -import getPleasingColors from "@repo/utils/getPleasingColors"; +import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; // TODO REPLACE WITH TLDRAW DEFAULTS // https://github.com/tldraw/tldraw/pull/1580/files @@ -58,7 +57,7 @@ const TEXT_PROPS = { padding: "0px", maxWidth: "auto", }; -const FONT_SIZES: Record = { +export const FONT_SIZES: Record = { m: 25, l: 38, xl: 48, @@ -343,26 +342,7 @@ export class BaseDiscourseNodeUtil extends ShapeUtil { } getColors() { - const { - canvasSettings: { color: setColor = "" } = {}, - index: discourseNodeIndex = -1, - } = discourseContext.nodes[this.type] || {}; - const paletteColor = - COLOR_ARRAY[ - discourseNodeIndex >= 0 && discourseNodeIndex < COLOR_ARRAY.length - 1 - ? discourseNodeIndex - : 0 - ]; - const formattedTextColor = - setColor && !setColor.startsWith("#") ? `#${setColor}` : setColor; - - const canvasSelectedColor = formattedTextColor - ? formattedTextColor - : COLOR_PALETTE[paletteColor]; - const pleasingColors = getPleasingColors(colord(canvasSelectedColor)); - const backgroundColor = pleasingColors.background; - const textColor = pleasingColors.text; - return { backgroundColor, textColor }; + return getDiscourseNodeColors({ nodeType: this.type }); } async toSvg(shape: DiscourseNodeShape): Promise { diff --git a/apps/roam/src/utils/getDiscourseNodeColors.ts b/apps/roam/src/utils/getDiscourseNodeColors.ts new file mode 100644 index 000000000..e41e78a72 --- /dev/null +++ b/apps/roam/src/utils/getDiscourseNodeColors.ts @@ -0,0 +1,41 @@ +import { colord } from "colord"; +import getPleasingColors from "@repo/utils/getPleasingColors"; +import { + COLOR_ARRAY, + COLOR_PALETTE, +} from "~/components/canvas/DiscourseNodeUtil"; +import getDiscourseNodes, { DiscourseNode } from "./getDiscourseNodes"; + +type GetDiscourseNodeColorsParams = { + nodeType?: string; + discourseNodes?: DiscourseNode[]; +}; + +export const getDiscourseNodeColors = ({ + nodeType, + discourseNodes = getDiscourseNodes(), +}: GetDiscourseNodeColorsParams): { + backgroundColor: string; + textColor: string; +} => { + const discourseNodeIndex = + discourseNodes.findIndex((node) => node.type === nodeType) ?? -1; + const color = discourseNodes[discourseNodeIndex]?.canvasSettings?.color ?? ""; + + const paletteColor = + COLOR_ARRAY[ + discourseNodeIndex >= 0 && discourseNodeIndex < COLOR_ARRAY.length - 1 + ? discourseNodeIndex + : 0 + ]; + const formattedTextColor = + color && !color.startsWith("#") ? `#${color}` : color; + + const canvasSelectedColor = formattedTextColor + ? formattedTextColor + : COLOR_PALETTE[paletteColor]; + const pleasingColors = getPleasingColors(colord(canvasSelectedColor)); + const backgroundColor = pleasingColors.background; + const textColor = pleasingColors.text; + return { backgroundColor, textColor }; +}; From 0e1c1ba4a885d1722434e53402ff382ef95ee830 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 1 Dec 2025 13:44:54 -0600 Subject: [PATCH 2/4] Enhance DiscourseToolPanel: Integrate getDiscourseNodeColors for improved color handling, adjust drag behavior for relations, and refine drag preview styling based on zoom level. --- .../components/canvas/DiscourseToolPanel.tsx | 119 ++++++++++++------ apps/roam/src/components/canvas/Tldraw.tsx | 4 +- 2 files changed, 81 insertions(+), 42 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index ca19bccaa..6ce9f7a05 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -6,12 +6,16 @@ import { Vec, Box, createShapeId, + FONT_FAMILIES, } from "tldraw"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil"; import { useAtom } from "@tldraw/state"; import { TOOL_ARROW_ICON_SVG, NODE_COLOR_ICON_SVG } from "~/icons"; +import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; +import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "./Tldraw"; +import { DEFAULT_STYLE_PROPS, FONT_SIZES } from "./DiscourseNodeUtil"; export type DiscourseGraphPanelProps = { nodes: DiscourseNode[]; @@ -30,6 +34,8 @@ type DragState = type: "node" | "relation"; id: string; text: string; + backgroundColor: string; + textColor: string; color: string; }; startPosition: Vec; @@ -40,6 +46,8 @@ type DragState = type: "node" | "relation"; id: string; text: string; + backgroundColor: string; + textColor: string; color: string; }; currentPosition: Vec; @@ -72,20 +80,32 @@ const DiscourseGraphPanel = ({ ); const panelItems = useMemo(() => { - const nodeItems = nodes.map((node) => ({ - type: "node" as const, - id: node.type, - text: node.text, - color: formatHexColor(node.canvasSettings.color) || "black", - shortcut: node.shortcut, - })); - - const relationItems = uniqueRelations.map((relation, index) => ({ - type: "relation" as const, - id: relation, - text: relation, - color: getRelationColor(relation, index), - })); + const nodeItems = nodes.map((node) => { + const { backgroundColor, textColor } = getDiscourseNodeColors({ + nodeType: node.type, + }); + return { + type: "node" as const, + id: node.type, + text: node.text, + backgroundColor: backgroundColor, + textColor: textColor, + color: formatHexColor(node.canvasSettings.color) || "black", + shortcut: node.shortcut, + }; + }); + + const relationItems = uniqueRelations.map((relation, index) => { + const color = getRelationColor(relation, index); + return { + type: "relation" as const, + id: relation, + text: relation, + backgroundColor: color, + textColor: "black", + color: color, + }; + }); return [...nodeItems, ...relationItems]; }, [nodes, uniqueRelations]); @@ -108,6 +128,10 @@ const DiscourseGraphPanel = ({ break; } case "pointing_item": { + // Relations should not be draggable + if (current.item.type === "relation") { + break; + } const dist = Vec.Dist(screenPoint, current.startPosition); if (dist > 10) { dragState.set({ @@ -153,14 +177,17 @@ const DiscourseGraphPanel = ({ case "dragging": { // When dragging ends, create the shape at the drop position const pagePoint = editor.screenToPage(current.currentPosition); + const zoomLevel = editor.getZoomLevel(); + const offsetX = DEFAULT_WIDTH / 2 / zoomLevel; + const offsetY = DEFAULT_HEIGHT / 2 / zoomLevel; if (current.item.type === "node") { const shapeId = createShapeId(); editor.createShape({ id: shapeId, type: current.item.id, - x: pagePoint.x, - y: pagePoint.y, + x: pagePoint.x - offsetX, + y: pagePoint.y - offsetY, props: { fontFamily: "sans", size: "s" }, }); editor.setEditingShape(shapeId); @@ -190,6 +217,11 @@ const DiscourseGraphPanel = ({ if (!item) return; + // Relations should not be draggable, only clickable + if (item.type === "relation") { + return; + } + const startPosition = new Vec(e.clientX, e.clientY); dragState.set({ @@ -228,6 +260,10 @@ const DiscourseGraphPanel = ({ const state = useValue("dragState", () => dragState.get(), [dragState]); + const zoomLevel = Math.max( + 0.5, + useValue("clipboardZoomLevel", () => editor.getZoomLevel(), [editor]), + ); // Drag preview management useQuickReactor( "drag-image-style", @@ -244,6 +280,11 @@ const DiscourseGraphPanel = ({ break; } case "dragging": { + // Relations should not be draggable + if (current.item.type === "relation") { + imageRef.style.display = "none"; + break; + } const panelContainerRect = panelContainerRef.getBoundingClientRect(); const box = new Box( panelContainerRect.x, @@ -251,32 +292,34 @@ const DiscourseGraphPanel = ({ panelContainerRect.width, panelContainerRect.height, ); - const viewportScreenBounds = editor.getViewportScreenBounds(); + + const zoomLevel = editor.getZoomLevel(); + const height = DEFAULT_HEIGHT * zoomLevel; + const width = DEFAULT_WIDTH * zoomLevel; const isInside = Box.ContainsPoint(box, current.currentPosition); if (isInside) { imageRef.style.display = "none"; } else { - imageRef.style.display = "block"; - imageRef.style.position = "absolute"; + const viewportScreenBounds = editor.getViewportScreenBounds(); + imageRef.style.display = "flex"; + imageRef.style.position = "fixed"; imageRef.style.pointerEvents = "none"; imageRef.style.left = "0px"; imageRef.style.top = "0px"; - imageRef.style.transform = `translate(${current.currentPosition.x - viewportScreenBounds.x - 25}px, ${current.currentPosition.y - viewportScreenBounds.y - 25}px)`; - imageRef.style.width = "50px"; - imageRef.style.height = "50px"; - imageRef.style.fontSize = "40px"; - imageRef.style.display = "flex"; - imageRef.style.alignItems = "center"; - imageRef.style.justifyContent = "center"; - imageRef.style.borderRadius = "8px"; - imageRef.style.backgroundColor = current.item.color; - imageRef.style.color = "white"; - imageRef.style.fontWeight = "bold"; + imageRef.style.transform = `translate(${current.currentPosition.x - viewportScreenBounds.x - width / 2}px, ${current.currentPosition.y - viewportScreenBounds.y - height / 2}px)`; + imageRef.style.width = `${width}px`; + imageRef.style.height = `${height}px`; + imageRef.style.zIndex = "9999"; + imageRef.style.borderRadius = `${16 * zoomLevel}px`; + imageRef.style.backgroundColor = current.item.backgroundColor; + imageRef.style.color = current.item.textColor; + imageRef.className = + "roamjs-tldraw-node pointer-events-none flex fixed items-center justify-center overflow-hidden"; } } } }, - [dragState], + [dragState, editor], ); // If it's a node tool, show only that node @@ -384,15 +427,11 @@ const DiscourseGraphPanel = ({ {state.name === "dragging" && (
{state.item.text} diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index f58727d92..db5522e7c 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -114,8 +114,8 @@ export const discourseContext: DiscourseContextType = { lastActions: [], }; -const DEFAULT_WIDTH = 160; -const DEFAULT_HEIGHT = 64; +export const DEFAULT_WIDTH = 160; +export const DEFAULT_HEIGHT = 64; export const MAX_WIDTH = "400px"; export const isPageUid = (uid: string) => From de83b06689077d822c683786a48c178ee03e3f0e Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 1 Dec 2025 14:05:01 -0600 Subject: [PATCH 3/4] Refactor DiscourseToolPanel: Simplify drag behavior by removing zoom-dependent offsets, adjust padding in drag preview, and enhance color retrieval logic in getDiscourseNodeColors for improved palette handling. --- apps/roam/src/components/canvas/DiscourseToolPanel.tsx | 10 +++++----- apps/roam/src/utils/getDiscourseNodeColors.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index 6ce9f7a05..094eac4db 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -177,9 +177,8 @@ const DiscourseGraphPanel = ({ case "dragging": { // When dragging ends, create the shape at the drop position const pagePoint = editor.screenToPage(current.currentPosition); - const zoomLevel = editor.getZoomLevel(); - const offsetX = DEFAULT_WIDTH / 2 / zoomLevel; - const offsetY = DEFAULT_HEIGHT / 2 / zoomLevel; + const offsetX = DEFAULT_WIDTH / 2; + const offsetY = DEFAULT_HEIGHT / 2; if (current.item.type === "node") { const shapeId = createShapeId(); @@ -264,6 +263,7 @@ const DiscourseGraphPanel = ({ 0.5, useValue("clipboardZoomLevel", () => editor.getZoomLevel(), [editor]), ); + // Drag preview management useQuickReactor( "drag-image-style", @@ -293,7 +293,7 @@ const DiscourseGraphPanel = ({ panelContainerRect.height, ); - const zoomLevel = editor.getZoomLevel(); + const zoomLevel = Math.max(0.5, editor.getZoomLevel()); const height = DEFAULT_HEIGHT * zoomLevel; const width = DEFAULT_WIDTH * zoomLevel; const isInside = Box.ContainsPoint(box, current.currentPosition); @@ -431,7 +431,7 @@ const DiscourseGraphPanel = ({ maxWidth: "", fontFamily: FONT_FAMILIES.sans, fontSize: `${FONT_SIZES.s * zoomLevel}px`, - padding: `${40 * zoomLevel}px`, + padding: `0px`, }} > {state.item.text} diff --git a/apps/roam/src/utils/getDiscourseNodeColors.ts b/apps/roam/src/utils/getDiscourseNodeColors.ts index e41e78a72..2a70badf8 100644 --- a/apps/roam/src/utils/getDiscourseNodeColors.ts +++ b/apps/roam/src/utils/getDiscourseNodeColors.ts @@ -24,7 +24,7 @@ export const getDiscourseNodeColors = ({ const paletteColor = COLOR_ARRAY[ - discourseNodeIndex >= 0 && discourseNodeIndex < COLOR_ARRAY.length - 1 + discourseNodeIndex >= 0 && discourseNodeIndex < COLOR_ARRAY.length ? discourseNodeIndex : 0 ]; From 9fa66a19a126fa4d0c0ef2ac698453b2a19fecef Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Mon, 1 Dec 2025 14:08:01 -0600 Subject: [PATCH 4/4] Refactor DiscourseToolPanel: Update drag behavior to ensure relations are only clickable, preventing pointermove listener from being added for relation items. --- .../src/components/canvas/DiscourseToolPanel.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx index 094eac4db..25b7ea540 100644 --- a/apps/roam/src/components/canvas/DiscourseToolPanel.tsx +++ b/apps/roam/src/components/canvas/DiscourseToolPanel.tsx @@ -216,11 +216,6 @@ const DiscourseGraphPanel = ({ if (!item) return; - // Relations should not be draggable, only clickable - if (item.type === "relation") { - return; - } - const startPosition = new Vec(e.clientX, e.clientY); dragState.set({ @@ -229,8 +224,12 @@ const DiscourseGraphPanel = ({ startPosition, }); - target.addEventListener("pointermove", handlePointerMove); - document.addEventListener("keydown", handleKeyDown); + // Relations should not be draggable, only clickable + // So we don't add the pointermove listener for relations + if (item.type !== "relation") { + target.addEventListener("pointermove", handlePointerMove); + document.addEventListener("keydown", handleKeyDown); + } }; const handleKeyDown = (e: KeyboardEvent) => {