From 4053aaf3635cff44b19118d9adf79034ee528446 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 22 Jan 2026 02:18:56 +0100 Subject: [PATCH 1/6] Add visible viewport to EditorContext and display ReactFlow MiniMap - Add VisibleViewport type representing panel insets (top, right, bottom, left) - Add computeVisibleViewport() helper that calculates insets based on panel state - Expose visibleViewport in EditorContext, computed reactively in EditorProvider - Add ReactFlow MiniMap positioned at top-right of visible viewport Co-Authored-By: Claude Opus 4.5 --- .../petrinaut/src/state/editor-context.ts | 13 +++++ .../petrinaut/src/state/editor-provider.tsx | 47 ++++++++++++++++++- .../petrinaut/src/views/SDCPN/sdcpn-view.tsx | 14 +++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/state/editor-context.ts b/libs/@hashintel/petrinaut/src/state/editor-context.ts index 0943af7f7a7..d1c251382ec 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-context.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-context.ts @@ -6,6 +6,17 @@ import { DEFAULT_PROPERTIES_PANEL_WIDTH, } from "../constants/ui"; +/** + * Represents the insets from each edge of the canvas that are covered by panels. + * These values indicate how much of the canvas is obscured from each direction. + */ +export type VisibleViewport = { + top: number; + right: number; + bottom: number; + left: number; +}; + export type DraggingStateByNodeId = Record< string, { dragging: boolean; position: { x: number; y: number } } @@ -36,6 +47,7 @@ export type EditorState = { selectedItemIds: Set; draggingStateByNodeId: DraggingStateByNodeId; timelineChartType: TimelineChartType; + visibleViewport: VisibleViewport; }; /** @@ -80,6 +92,7 @@ export const initialEditorState: EditorState = { selectedItemIds: new Set(), draggingStateByNodeId: {}, timelineChartType: "run", + visibleViewport: { top: 0, right: 0, bottom: 0, left: 0 }, }; const DEFAULT_CONTEXT_VALUE: EditorContextValue = { diff --git a/libs/@hashintel/petrinaut/src/state/editor-provider.tsx b/libs/@hashintel/petrinaut/src/state/editor-provider.tsx index f83c30b52b8..762ecf8ba79 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-provider.tsx +++ b/libs/@hashintel/petrinaut/src/state/editor-provider.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; +import { PANEL_MARGIN } from "../constants/ui"; import { type DraggingStateByNodeId, type EditorActions, @@ -7,10 +8,33 @@ import { type EditorContextValue, type EditorState, initialEditorState, + type VisibleViewport, } from "./editor-context"; export type EditorProviderProps = React.PropsWithChildren; +function computeVisibleViewport(params: { + isLeftSidebarOpen: boolean; + leftSidebarWidth: number; + selectedResourceId: string | null; + propertiesPanelWidth: number; + isBottomPanelOpen: boolean; + bottomPanelHeight: number; +}): VisibleViewport { + return { + top: PANEL_MARGIN, + left: params.isLeftSidebarOpen + ? params.leftSidebarWidth + PANEL_MARGIN * 2 + : PANEL_MARGIN, + right: params.selectedResourceId + ? params.propertiesPanelWidth + PANEL_MARGIN * 2 + : PANEL_MARGIN, + bottom: params.isBottomPanelOpen + ? params.bottomPanelHeight + PANEL_MARGIN * 2 + : PANEL_MARGIN, + }; +} + export const EditorProvider: React.FC = ({ children }) => { const [state, setState] = useState(initialEditorState); @@ -68,9 +92,30 @@ export const EditorProvider: React.FC = ({ children }) => { __reinitialize: () => setState(initialEditorState), }; + const visibleViewport = useMemo( + () => + computeVisibleViewport({ + isLeftSidebarOpen: state.isLeftSidebarOpen, + leftSidebarWidth: state.leftSidebarWidth, + selectedResourceId: state.selectedResourceId, + propertiesPanelWidth: state.propertiesPanelWidth, + isBottomPanelOpen: state.isBottomPanelOpen, + bottomPanelHeight: state.bottomPanelHeight, + }), + [ + state.isLeftSidebarOpen, + state.leftSidebarWidth, + state.selectedResourceId, + state.propertiesPanelWidth, + state.isBottomPanelOpen, + state.bottomPanelHeight, + ], + ); + const contextValue: EditorContextValue = { ...state, ...actions, + visibleViewport, }; return ( diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index f0f7fb4fdb8..b6e58b24285 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -3,9 +3,10 @@ import "reactflow/dist/style.css"; import { css } from "@hashintel/ds-helpers/css"; import { use, useEffect, useRef, useState } from "react"; import type { Connection, Node, ReactFlowInstance } from "reactflow"; -import ReactFlow, { Background, ConnectionLineType } from "reactflow"; +import ReactFlow, { Background, ConnectionLineType, MiniMap } from "reactflow"; import { v4 as generateUuid } from "uuid"; +import { PANEL_MARGIN } from "../../constants/ui"; import { DEFAULT_TRANSITION_KERNEL_CODE, generateDefaultLambdaCode, @@ -71,6 +72,7 @@ export const SDCPNView: React.FC = () => { setSelectedItemIds, setSelectedResourceId, clearSelection, + visibleViewport, } = use(EditorContext); // Hook for applying node changes @@ -341,6 +343,16 @@ export const SDCPNView: React.FC = () => { zoomOnScroll > + ); From 2368f9a7a4097167468b10d2763bb6ffda55f0a6 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 27 Jan 2026 01:15:52 +0100 Subject: [PATCH 2/6] Extract MiniMap to separate component with custom styling - Create dedicated MiniMap wrapper component - Render place nodes as circles (transitions as rectangles) - Add rounded corners (6px) to minimap SVG - Set fixed size (100x64px) with reduced padding - Position relative to visible viewport for panel awareness Co-Authored-By: Claude Opus 4.5 --- .../src/views/SDCPN/components/mini-map.tsx | 67 +++++++++++++++++++ .../petrinaut/src/views/SDCPN/sdcpn-view.tsx | 16 +---- 2 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx new file mode 100644 index 00000000000..d19aa73be81 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -0,0 +1,67 @@ +import { css } from "@hashintel/ds-helpers/css"; +import { use } from "react"; +import type { MiniMapNodeProps, MiniMapProps } from "reactflow"; +import { MiniMap as ReactFlowMiniMap, useStore } from "reactflow"; + +import { EditorContext } from "../../../state/editor-context"; +import type { NodeType } from "../reactflow-types"; + +const miniMapClassName = css({ + "& svg": { + borderRadius: 6, + }, +}); + +/** + * Custom node renderer for the MiniMap. + * Renders place nodes as circles and transition nodes as rectangles. + */ +const MiniMapNode: React.FC = ({ + id, + x, + y, + width, + height, + color, +}) => { + // MiniMapNodeProps doesn't include node data, so we look it up from the store + const node = useStore( + (state) => state.nodeInternals.get(id) as NodeType | undefined, + ); + + if (node?.data.type === "place") { + const radius = Math.min(width, height) / 2; + return ( + + ); + } + + return ; +}; + +/** + * A simple wrapper around ReactFlow's MiniMap that positions it + * to account for panel insets (left sidebar, properties panel, bottom panel). + */ +export const MiniMap: React.FC> = (props) => { + const { visibleViewport } = use(EditorContext); + + return ( + + ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index b6e58b24285..eb48a7c1eb3 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -3,10 +3,9 @@ import "reactflow/dist/style.css"; import { css } from "@hashintel/ds-helpers/css"; import { use, useEffect, useRef, useState } from "react"; import type { Connection, Node, ReactFlowInstance } from "reactflow"; -import ReactFlow, { Background, ConnectionLineType, MiniMap } from "reactflow"; +import ReactFlow, { Background, ConnectionLineType } from "reactflow"; import { v4 as generateUuid } from "uuid"; -import { PANEL_MARGIN } from "../../constants/ui"; import { DEFAULT_TRANSITION_KERNEL_CODE, generateDefaultLambdaCode, @@ -15,6 +14,7 @@ import { EditorContext } from "../../state/editor-context"; import { SDCPNContext } from "../../state/sdcpn-context"; import { useIsReadOnly } from "../../state/use-is-read-only"; import { Arc } from "./components/arc"; +import { MiniMap } from "./components/mini-map"; import { PlaceNode } from "./components/place-node"; import { TransitionNode } from "./components/transition-node"; import { useApplyNodeChanges } from "./hooks/use-apply-node-changes"; @@ -72,7 +72,6 @@ export const SDCPNView: React.FC = () => { setSelectedItemIds, setSelectedResourceId, clearSelection, - visibleViewport, } = use(EditorContext); // Hook for applying node changes @@ -343,16 +342,7 @@ export const SDCPNView: React.FC = () => { zoomOnScroll > - + ); From 1f256c2a4ec4aaaf943b031289bf79128ab976e8 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 27 Jan 2026 02:04:15 +0100 Subject: [PATCH 3/6] Remove visibleViewport dependency from MiniMap Simplify MiniMap to use default ReactFlow positioning for now. Viewport-aware positioning will be added in a follow-up PR. Co-Authored-By: Claude Opus 4.5 --- .../petrinaut/src/state/editor-context.ts | 13 ----- .../petrinaut/src/state/editor-provider.tsx | 47 +------------------ .../src/views/SDCPN/components/mini-map.tsx | 12 +---- 3 files changed, 3 insertions(+), 69 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/state/editor-context.ts b/libs/@hashintel/petrinaut/src/state/editor-context.ts index d1c251382ec..0943af7f7a7 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-context.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-context.ts @@ -6,17 +6,6 @@ import { DEFAULT_PROPERTIES_PANEL_WIDTH, } from "../constants/ui"; -/** - * Represents the insets from each edge of the canvas that are covered by panels. - * These values indicate how much of the canvas is obscured from each direction. - */ -export type VisibleViewport = { - top: number; - right: number; - bottom: number; - left: number; -}; - export type DraggingStateByNodeId = Record< string, { dragging: boolean; position: { x: number; y: number } } @@ -47,7 +36,6 @@ export type EditorState = { selectedItemIds: Set; draggingStateByNodeId: DraggingStateByNodeId; timelineChartType: TimelineChartType; - visibleViewport: VisibleViewport; }; /** @@ -92,7 +80,6 @@ export const initialEditorState: EditorState = { selectedItemIds: new Set(), draggingStateByNodeId: {}, timelineChartType: "run", - visibleViewport: { top: 0, right: 0, bottom: 0, left: 0 }, }; const DEFAULT_CONTEXT_VALUE: EditorContextValue = { diff --git a/libs/@hashintel/petrinaut/src/state/editor-provider.tsx b/libs/@hashintel/petrinaut/src/state/editor-provider.tsx index 762ecf8ba79..f83c30b52b8 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-provider.tsx +++ b/libs/@hashintel/petrinaut/src/state/editor-provider.tsx @@ -1,6 +1,5 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; -import { PANEL_MARGIN } from "../constants/ui"; import { type DraggingStateByNodeId, type EditorActions, @@ -8,33 +7,10 @@ import { type EditorContextValue, type EditorState, initialEditorState, - type VisibleViewport, } from "./editor-context"; export type EditorProviderProps = React.PropsWithChildren; -function computeVisibleViewport(params: { - isLeftSidebarOpen: boolean; - leftSidebarWidth: number; - selectedResourceId: string | null; - propertiesPanelWidth: number; - isBottomPanelOpen: boolean; - bottomPanelHeight: number; -}): VisibleViewport { - return { - top: PANEL_MARGIN, - left: params.isLeftSidebarOpen - ? params.leftSidebarWidth + PANEL_MARGIN * 2 - : PANEL_MARGIN, - right: params.selectedResourceId - ? params.propertiesPanelWidth + PANEL_MARGIN * 2 - : PANEL_MARGIN, - bottom: params.isBottomPanelOpen - ? params.bottomPanelHeight + PANEL_MARGIN * 2 - : PANEL_MARGIN, - }; -} - export const EditorProvider: React.FC = ({ children }) => { const [state, setState] = useState(initialEditorState); @@ -92,30 +68,9 @@ export const EditorProvider: React.FC = ({ children }) => { __reinitialize: () => setState(initialEditorState), }; - const visibleViewport = useMemo( - () => - computeVisibleViewport({ - isLeftSidebarOpen: state.isLeftSidebarOpen, - leftSidebarWidth: state.leftSidebarWidth, - selectedResourceId: state.selectedResourceId, - propertiesPanelWidth: state.propertiesPanelWidth, - isBottomPanelOpen: state.isBottomPanelOpen, - bottomPanelHeight: state.bottomPanelHeight, - }), - [ - state.isLeftSidebarOpen, - state.leftSidebarWidth, - state.selectedResourceId, - state.propertiesPanelWidth, - state.isBottomPanelOpen, - state.bottomPanelHeight, - ], - ); - const contextValue: EditorContextValue = { ...state, ...actions, - visibleViewport, }; return ( diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx index d19aa73be81..e79571c77b0 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -1,9 +1,7 @@ import { css } from "@hashintel/ds-helpers/css"; -import { use } from "react"; import type { MiniMapNodeProps, MiniMapProps } from "reactflow"; import { MiniMap as ReactFlowMiniMap, useStore } from "reactflow"; -import { EditorContext } from "../../../state/editor-context"; import type { NodeType } from "../reactflow-types"; const miniMapClassName = css({ @@ -40,21 +38,15 @@ const MiniMapNode: React.FC = ({ }; /** - * A simple wrapper around ReactFlow's MiniMap that positions it - * to account for panel insets (left sidebar, properties panel, bottom panel). + * A wrapper around ReactFlow's MiniMap with custom styling. + * Renders place nodes as circles and transition nodes as rectangles. */ export const MiniMap: React.FC> = (props) => { - const { visibleViewport } = use(EditorContext); - return ( Date: Tue, 27 Jan 2026 02:23:27 +0100 Subject: [PATCH 4/6] Position MiniMap at top-right, offset by properties panel - Place MiniMap at top of canvas instead of bottom - Offset from right edge by properties panel width when visible Co-Authored-By: Claude Opus 4.5 --- .../src/views/SDCPN/components/mini-map.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx index e79571c77b0..a80527eef74 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -1,7 +1,10 @@ import { css } from "@hashintel/ds-helpers/css"; +import { use } from "react"; import type { MiniMapNodeProps, MiniMapProps } from "reactflow"; import { MiniMap as ReactFlowMiniMap, useStore } from "reactflow"; +import { PANEL_MARGIN } from "../../../constants/ui"; +import { EditorContext } from "../../../state/editor-context"; import type { NodeType } from "../reactflow-types"; const miniMapClassName = css({ @@ -40,13 +43,25 @@ const MiniMapNode: React.FC = ({ /** * A wrapper around ReactFlow's MiniMap with custom styling. * Renders place nodes as circles and transition nodes as rectangles. + * Positions at top-right, offset by properties panel width when visible. */ export const MiniMap: React.FC> = (props) => { + const { selectedResourceId, propertiesPanelWidth } = use(EditorContext); + + const isPropertiesPanelVisible = selectedResourceId !== null; + const rightOffset = isPropertiesPanelVisible + ? propertiesPanelWidth + PANEL_MARGIN * 2 + : PANEL_MARGIN; + return ( Date: Wed, 28 Jan 2026 00:38:02 +0100 Subject: [PATCH 5/6] Use design token for MiniMap border radius Co-Authored-By: Claude Opus 4.5 --- .../petrinaut/src/views/SDCPN/components/mini-map.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx index a80527eef74..96ddba40b60 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -9,7 +9,7 @@ import type { NodeType } from "../reactflow-types"; const miniMapClassName = css({ "& svg": { - borderRadius: 6, + borderRadius: "md.3", }, }); From 3ba9d4bcb0249721a1ab871ca3113601112307b2 Mon Sep 17 00:00:00 2001 From: Ciaran Morinan <37743469+CiaranMn@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:18:13 +0000 Subject: [PATCH 6/6] Add MiniMap colours, tweak size/ratio (#8335) --- .../src/views/SDCPN/components/mini-map.tsx | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx index 96ddba40b60..9c110e6b80b 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -1,9 +1,10 @@ import { css } from "@hashintel/ds-helpers/css"; -import { use } from "react"; +import { use, useMemo } from "react"; import type { MiniMapNodeProps, MiniMapProps } from "reactflow"; import { MiniMap as ReactFlowMiniMap, useStore } from "reactflow"; import { PANEL_MARGIN } from "../../../constants/ui"; +import { hexToHsl } from "../../../lib/hsl-color"; import { EditorContext } from "../../../state/editor-context"; import type { NodeType } from "../reactflow-types"; @@ -13,6 +14,13 @@ const miniMapClassName = css({ }, }); +/** Default colors for nodes without a type color */ +const DEFAULT_PLACE_FILL = "#f8f8f8"; +const DEFAULT_PLACE_STROKE = "#666666"; +const DEFAULT_TRANSITION_FILL = "#6b7280"; + +const PLACE_STROKE_WIDTH = 2; + /** * Custom node renderer for the MiniMap. * Renders place nodes as circles and transition nodes as rectangles. @@ -23,21 +31,47 @@ const MiniMapNode: React.FC = ({ y, width, height, - color, }) => { // MiniMapNodeProps doesn't include node data, so we look it up from the store const node = useStore( (state) => state.nodeInternals.get(id) as NodeType | undefined, ); + // Compute colors based on node type and type color + const { fill, stroke } = useMemo(() => { + if (node?.data.type === "place") { + const typeColor = node.data.typeColor; + + if (typeColor) { + const hsl = hexToHsl(typeColor); + return { + fill: hsl.lighten(20).css(0.9), + stroke: hsl.lighten(-15).saturate(-20).css(1), + }; + } + + return { fill: DEFAULT_PLACE_FILL, stroke: DEFAULT_PLACE_STROKE }; + } + + // Transitions: solid grey with no stroke + return { fill: DEFAULT_TRANSITION_FILL, stroke: undefined }; + }, [node?.data]); + if (node?.data.type === "place") { - const radius = Math.min(width, height) / 2; + const radius = Math.min(width, height) / 2 - PLACE_STROKE_WIDTH / 2; return ( - + ); } - return ; + return ; }; /** @@ -56,14 +90,15 @@ export const MiniMap: React.FC> = (props) => { return (