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..9c110e6b80b --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/mini-map.tsx @@ -0,0 +1,109 @@ +import { css } from "@hashintel/ds-helpers/css"; +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"; + +const miniMapClassName = css({ + "& svg": { + borderRadius: "md.3", + }, +}); + +/** 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. + */ +const MiniMapNode: React.FC = ({ + id, + x, + y, + width, + height, +}) => { + // 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 - PLACE_STROKE_WIDTH / 2; + return ( + + ); + } + + return ; +}; + +/** + * 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 ( + + ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index f0f7fb4fdb8..eb48a7c1eb3 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -14,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"; @@ -341,6 +342,7 @@ export const SDCPNView: React.FC = () => { zoomOnScroll > + );