From 289cf54e87b6b46bfb5d679a4d9bc446cd458d92 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 02:11:23 +0100
Subject: [PATCH 01/29] Remove DisabledTooltip wrapper and inline tooltip logic

- Create CodeEditor component wrapping Monaco Editor with tooltip support
- Add tooltip prop to Switch component
- Update Tooltip component to optionally render based on content
- Refactor property panels to use inline tooltip pattern:
  `<Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>`
- Delete disabled-tooltip.tsx as it's no longer needed

This ensures tooltips show reliably on disabled elements by having
the Tooltip wrapper handle hover events instead of relying on disabled
elements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/code-editor.tsx  |  80 +++++++++++
 .../src/components/disabled-tooltip.tsx       |  43 ------
 .../petrinaut/src/components/switch.tsx       |  12 +-
 .../petrinaut/src/components/tooltip.tsx      |   9 +-
 .../differential-equation-properties.tsx      |  82 +++--------
 .../PropertiesPanel/parameter-properties.tsx  |  15 +-
 .../PropertiesPanel/place-properties.tsx      |  40 +++---
 .../PropertiesPanel/transition-properties.tsx | 134 +++++-------------
 .../PropertiesPanel/type-properties.tsx       |  29 ++--
 9 files changed, 209 insertions(+), 235 deletions(-)
 create mode 100644 libs/@hashintel/petrinaut/src/components/code-editor.tsx
 delete mode 100644 libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx

diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
new file mode 100644
index 00000000000..8f8e636f03e
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -0,0 +1,80 @@
+import { css, cva } from "@hashintel/ds-helpers/css";
+import type { EditorProps } from "@monaco-editor/react";
+import MonacoEditor from "@monaco-editor/react";
+
+import { Tooltip } from "./tooltip";
+
+const containerStyle = cva({
+  base: {
+    position: "relative",
+    border: "[1px solid rgba(0, 0, 0, 0.1)]",
+    borderRadius: "[4px]",
+    overflow: "hidden",
+  },
+  variants: {
+    isReadOnly: {
+      true: {
+        filter: "[grayscale(20%) brightness(98%)]",
+      },
+      false: {},
+    },
+  },
+});
+
+const tooltipOverlayStyle = css({
+  position: "absolute",
+  inset: "[0]",
+  zIndex: "[1]",
+  cursor: "not-allowed",
+});
+
+type CodeEditorProps = Omit<EditorProps, "theme"> & {
+  tooltip?: string;
+};
+
+/**
+ * Code editor component that wraps Monaco Editor.
+ *
+ * @param tooltip - Optional tooltip to show when hovering over the editor.
+ *                  When provided, the editor becomes non-interactive (for read-only mode explanations).
+ */
+export const CodeEditor: React.FC<CodeEditorProps> = ({
+  tooltip,
+  options,
+  height,
+  ...props
+}) => {
+  const isReadOnly = options?.readOnly === true;
+
+  const editorOptions: EditorProps["options"] = {
+    minimap: { enabled: false },
+    scrollBeyondLastLine: false,
+    fontSize: 12,
+    lineNumbers: "off",
+    folding: true,
+    glyphMargin: false,
+    lineDecorationsWidth: 0,
+    lineNumbersMinChars: 3,
+    padding: { top: 8, bottom: 8 },
+    fixedOverflowWidgets: true,
+    ...options,
+  };
+
+  const editor = (
+    <div className={containerStyle({ isReadOnly })} style={{ height }}>
+      <MonacoEditor
+        theme="vs-light"
+        height="100%"
+        options={editorOptions}
+        {...props}
+      />
+      {tooltip && <div className={tooltipOverlayStyle} />}
+    </div>
+  );
+
+  if (tooltip) {
+    return <Tooltip content={tooltip}>{editor}</Tooltip>;
+  }
+
+  return editor;
+};
diff --git a/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx b/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx
deleted file mode 100644
index 8590a0f1785..00000000000
--- a/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import type { ReactNode } from "react";
-
-import { UI_MESSAGES } from "../constants/ui-messages";
-import { Tooltip } from "./tooltip";
-
-interface DisabledTooltipProps {
-  /**
-   * Whether the wrapped element is disabled.
-   * When true, a tooltip explaining why will be shown on hover.
-   */
-  disabled: boolean;
-  /**
-   * The content to wrap. Should be a single element.
-   */
-  children: ReactNode;
-  /**
-   * Optional custom message. Defaults to the standard readonly mode message.
-   */
-  message?: string;
-}
-
-/**
- * Wraps children with an explanatory tooltip when disabled.
- *
- * Use this to wrap disabled form controls to explain why they're disabled
- * (e.g., during simulation mode).
- *
- * @example
- * <DisabledTooltip disabled={isReadOnly}>
- *   <input disabled={isReadOnly} ... />
- * </DisabledTooltip>
- */
-export const DisabledTooltip: React.FC<DisabledTooltipProps> = ({
-  disabled,
-  children,
-  message = UI_MESSAGES.READ_ONLY_MODE,
-}) => {
-  if (!disabled) {
-    return children;
-  }
-
-  return <Tooltip content={message}>{children}</Tooltip>;
-};
diff --git a/libs/@hashintel/petrinaut/src/components/switch.tsx b/libs/@hashintel/petrinaut/src/components/switch.tsx
index d0bb2ac4ac8..2184e53ec0b 100644
--- a/libs/@hashintel/petrinaut/src/components/switch.tsx
+++ b/libs/@hashintel/petrinaut/src/components/switch.tsx
@@ -1,18 +1,22 @@
 import { Switch as ArkSwitch } from "@ark-ui/react/switch";
 import { css } from "@hashintel/ds-helpers/css";
 
+import { Tooltip } from "./tooltip";
+
 interface SwitchProps {
   checked?: boolean;
   onCheckedChange?: (checked: boolean) => void;
   disabled?: boolean;
+  tooltip?: string;
 }
 
 export const Switch: React.FC<SwitchProps> = ({
   checked,
   onCheckedChange,
   disabled = false,
+  tooltip,
 }) => {
-  return (
+  const switchElement = (
     <ArkSwitch.Root
       checked={checked}
       onCheckedChange={(details) => {
@@ -55,4 +59,10 @@ export const Switch: React.FC<SwitchProps> = ({
       <ArkSwitch.HiddenInput />
     </ArkSwitch.Root>
   );
+
+  if (tooltip) {
+    return <Tooltip content={tooltip}>{switchElement}</Tooltip>;
+  }
+
+  return switchElement;
 };
diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index f22f9c38cd2..61abc071929 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -15,11 +15,18 @@ const tooltipContentStyle = css({
 });
 
 interface TooltipProps {
-  content: string;
+  /**
+   * The tooltip content. When empty/undefined, children are rendered without tooltip wrapper.
+   */
+  content?: string;
   children: ReactNode;
 }
 
 export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
+  if (!content) {
+    return children;
+  }
+
   return (
     <ArkTooltip.Root
       openDelay={200}
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
index fe9e0238f2d..c88e80b57b8 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
@@ -1,11 +1,10 @@
 /* eslint-disable id-length */
 
 import { css, cva } from "@hashintel/ds-helpers/css";
-import MonacoEditor from "@monaco-editor/react";
 import { useState } from "react";
 import { TbDotsVertical, TbSparkles } from "react-icons/tb";
 
-import { DisabledTooltip } from "../../../../components/disabled-tooltip";
+import { CodeEditor } from "../../../../components/code-editor";
 import { Menu } from "../../../../components/menu";
 import { Tooltip } from "../../../../components/tooltip";
 import { UI_MESSAGES } from "../../../../constants/ui-messages";
@@ -223,28 +222,6 @@ const menuButtonStyle = css({
   color: "[rgba(0, 0, 0, 0.6)]",
 });
 
-const editorContainerStyle = cva({
-  base: {
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    overflow: "hidden",
-    flex: "[1]",
-    minHeight: "[0]",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        filter: "[grayscale(20%) brightness(98%)]",
-        pointerEvents: "none",
-      },
-      false: {
-        filter: "[none]",
-        pointerEvents: "auto",
-      },
-    },
-  },
-});
-
 const aiMenuItemStyle = css({
   display: "flex",
   alignItems: "center",
@@ -332,7 +309,7 @@ export const DifferentialEquationProperties: React.FC<
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <DisabledTooltip disabled={isReadOnly}>
+        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={differentialEquation.name}
@@ -347,13 +324,15 @@ export const DifferentialEquationProperties: React.FC<
             disabled={isReadOnly}
             className={inputStyle({ isReadOnly })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       <div>
         <div className={fieldLabelStyle}>Associated Type</div>
         <div style={{ position: "relative" }}>
-          <DisabledTooltip disabled={isReadOnly}>
+          <Tooltip
+            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          >
             <button
               type="button"
               onClick={() => setShowTypeDropdown(!showTypeDropdown)}
@@ -371,7 +350,7 @@ export const DifferentialEquationProperties: React.FC<
                 </>
               )}
             </button>
-          </DisabledTooltip>
+          </Tooltip>
           {showTypeDropdown && !isReadOnly && (
             <div className={dropdownMenuStyle}>
               {types.map((type) => (
@@ -519,37 +498,22 @@ export const DifferentialEquationProperties: React.FC<
             />
           )}
         </div>
-        <DisabledTooltip disabled={isReadOnly}>
-          <div className={editorContainerStyle({ isReadOnly })}>
-            <MonacoEditor
-              language="typescript"
-              value={differentialEquation.code}
-              onChange={(newCode) => {
-                updateDifferentialEquation(
-                  differentialEquation.id,
-                  (existingEquation) => {
-                    existingEquation.code = newCode ?? "";
-                  },
-                );
-              }}
-              path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`}
-              theme="vs-light"
-              options={{
-                minimap: { enabled: false },
-                scrollBeyondLastLine: false,
-                fontSize: 12,
-                lineNumbers: "off",
-                folding: true,
-                glyphMargin: false,
-                lineDecorationsWidth: 0,
-                lineNumbersMinChars: 3,
-                padding: { top: 8, bottom: 8 },
-                fixedOverflowWidgets: true,
-                readOnly: isReadOnly,
-              }}
-            />
-          </div>
-        </DisabledTooltip>
+        <CodeEditor
+          language="typescript"
+          value={differentialEquation.code}
+          height="100%"
+          onChange={(newCode) => {
+            updateDifferentialEquation(
+              differentialEquation.id,
+              (existingEquation) => {
+                existingEquation.code = newCode ?? "";
+              },
+            );
+          }}
+          path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`}
+          options={{ readOnly: isReadOnly }}
+          tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
     </div>
   );
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
index 231415dc6a8..c934c3bf1dc 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
@@ -1,6 +1,7 @@
 import { css, cva } from "@hashintel/ds-helpers/css";
 
-import { DisabledTooltip } from "../../../../components/disabled-tooltip";
+import { Tooltip } from "../../../../components/tooltip";
+import { UI_MESSAGES } from "../../../../constants/ui-messages";
 import type { Parameter } from "../../../../core/types/sdcpn";
 import { useIsReadOnly } from "../../../../state/use-is-read-only";
 
@@ -142,7 +143,7 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
       {/* Name field */}
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <DisabledTooltip disabled={isDisabled}>
+        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={parameter.name}
@@ -150,13 +151,13 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
             disabled={isDisabled}
             className={inputStyle({ isDisabled })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       {/* Variable Name field */}
       <div>
         <div className={fieldLabelStyle}>Variable Name</div>
-        <DisabledTooltip disabled={isDisabled}>
+        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={parameter.variableName}
@@ -165,7 +166,7 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
             disabled={isDisabled}
             className={inputStyle({ isDisabled, isMonospace: true })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       {/* Type selector - hidden for now as internal code relies on "real" type */}
@@ -173,7 +174,7 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
       {/* Default Value field */}
       <div>
         <div className={fieldLabelStyle}>Default Value</div>
-        <DisabledTooltip disabled={isDisabled}>
+        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={parameter.defaultValue}
@@ -181,7 +182,7 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
             disabled={isDisabled}
             className={inputStyle({ isDisabled, isMonospace: true })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
     </div>
   );
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index 62d8875fcac..1a23d5de963 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -9,7 +9,6 @@ import {
   TbTrash,
 } from "react-icons/tb";
 
-import { DisabledTooltip } from "../../../../components/disabled-tooltip";
 import { Menu } from "../../../../components/menu";
 import type { SubView } from "../../../../components/sub-view/types";
 import { FixedHeightSubViewsContainer } from "../../../../components/sub-view/vertical-sub-views-container";
@@ -407,7 +406,9 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
 
         <div>
           <div className={fieldLabelStyle}>Name</div>
-          <DisabledTooltip disabled={isReadOnly}>
+          <Tooltip
+            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          >
             <input
               ref={nameInputRef}
               type="text"
@@ -427,7 +428,7 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
               disabled={isReadOnly}
               className={inputStyle({ isReadOnly, hasError: !!nameError })}
             />
-          </DisabledTooltip>
+          </Tooltip>
           {nameError && <div className={errorMessageStyle}>{nameError}</div>}
         </div>
 
@@ -442,7 +443,9 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
               } Tokens in places don't have to carry data, but they need one to enable dynamics (token data changing over time when in a place).`}
             />
           </div>
-          <DisabledTooltip disabled={isReadOnly}>
+          <Tooltip
+            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          >
             <select
               value={place.colorId ?? ""}
               onChange={(event) => {
@@ -469,7 +472,7 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
                 </option>
               ))}
             </select>
-          </DisabledTooltip>
+          </Tooltip>
 
           {place.colorId && (
             <div className={jumpButtonContainerStyle}>
@@ -490,17 +493,16 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
         <div className={sectionContainerStyle}>
           <div className={switchRowStyle}>
             <div className={switchContainerStyle}>
-              <DisabledTooltip disabled={isReadOnly}>
-                <Switch
-                  checked={!!place.colorId && place.dynamicsEnabled}
-                  disabled={isReadOnly || place.colorId === null}
-                  onCheckedChange={(checked) => {
-                    updatePlace(place.id, (existingPlace) => {
-                      existingPlace.dynamicsEnabled = checked;
-                    });
-                  }}
-                />
-              </DisabledTooltip>
+              <Switch
+                checked={!!place.colorId && place.dynamicsEnabled}
+                disabled={isReadOnly || place.colorId === null}
+                tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                onCheckedChange={(checked) => {
+                  updatePlace(place.id, (existingPlace) => {
+                    existingPlace.dynamicsEnabled = checked;
+                  });
+                }}
+              />
             </div>
             <div className={fieldLabelWithTooltipStyle}>
               Dynamics
@@ -523,7 +525,9 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
           availableDiffEqs.length > 0 && (
             <div className={diffEqContainerStyle}>
               <div className={fieldLabelStyle}>Differential Equation</div>
-              <DisabledTooltip disabled={isReadOnly}>
+              <Tooltip
+                content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+              >
                 <select
                   value={place.differentialEquationId ?? undefined}
                   onChange={(event) => {
@@ -543,7 +547,7 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
                     </option>
                   ))}
                 </select>
-              </DisabledTooltip>
+              </Tooltip>
 
               {place.differentialEquationId && (
                 <div className={jumpButtonContainerStyle}>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index 83f4ce1dd97..e4db10204e1 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -16,11 +16,10 @@ import {
   verticalListSortingStrategy,
 } from "@dnd-kit/sortable";
 import { css, cva } from "@hashintel/ds-helpers/css";
-import MonacoEditor from "@monaco-editor/react";
 import { use } from "react";
 import { TbDotsVertical, TbSparkles, TbTrash } from "react-icons/tb";
 
-import { DisabledTooltip } from "../../../../components/disabled-tooltip";
+import { CodeEditor } from "../../../../components/code-editor";
 import { Menu } from "../../../../components/menu";
 import { SegmentGroup } from "../../../../components/segment-group";
 import { InfoIconTooltip, Tooltip } from "../../../../components/tooltip";
@@ -166,29 +165,6 @@ const menuButtonStyle = css({
   color: "[rgba(0, 0, 0, 0.6)]",
 });
 
-const editorContainerStyle = cva({
-  base: {
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    overflow: "hidden",
-  },
-  variants: {
-    size: {
-      lambda: { height: "[340px]" },
-      kernel: { height: "[400px]" },
-    },
-    isReadOnly: {
-      true: {
-        filter: "[grayscale(20%) brightness(98%)]",
-        pointerEvents: "none",
-      },
-      false: {
-        pointerEvents: "auto",
-      },
-    },
-  },
-});
-
 const aiMenuItemStyle = css({
   display: "flex",
   alignItems: "center",
@@ -365,7 +341,7 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <DisabledTooltip disabled={isReadOnly}>
+        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={transition.name}
@@ -377,7 +353,7 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
             disabled={isReadOnly}
             className={inputStyle({ isReadOnly })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       <div className={sectionContainerStyle}>
@@ -477,7 +453,7 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
           Firing time
           <InfoIconTooltip tooltip="Define the rate at or conditions under which this will transition will fire, optionally based on each set of input tokens' data (where input tokens have types)." />
         </div>
-        <DisabledTooltip disabled={isReadOnly}>
+        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <div className={segmentGroupWrapperStyle({ isReadOnly })}>
             <SegmentGroup
               value={transition.lambdaType}
@@ -494,7 +470,7 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
               }}
             />
           </div>
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       <div>
@@ -550,37 +526,22 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
             />
           )}
         </div>
-        <DisabledTooltip disabled={isReadOnly}>
-          <div className={editorContainerStyle({ isReadOnly, size: "lambda" })}>
-            <MonacoEditor
-              key={`lambda-${transition.lambdaType}-${transition.inputArcs
-                .map((a) => `${a.placeId}:${a.weight}`)
-                .join("-")}`}
-              language="typescript"
-              value={transition.lambdaCode || ""}
-              path={`inmemory://sdcpn/transitions/${transition.id}/lambda.ts`}
-              onChange={(value) => {
-                updateTransition(transition.id, (existingTransition) => {
-                  existingTransition.lambdaCode = value ?? "";
-                });
-              }}
-              theme="vs-light"
-              options={{
-                minimap: { enabled: false },
-                scrollBeyondLastLine: false,
-                fontSize: 12,
-                lineNumbers: "off",
-                folding: true,
-                glyphMargin: false,
-                lineDecorationsWidth: 0,
-                lineNumbersMinChars: 3,
-                padding: { top: 8, bottom: 8 },
-                readOnly: isReadOnly,
-                fixedOverflowWidgets: true,
-              }}
-            />
-          </div>
-        </DisabledTooltip>
+        <CodeEditor
+          key={`lambda-${transition.lambdaType}-${transition.inputArcs
+            .map((a) => `${a.placeId}:${a.weight}`)
+            .join("-")}`}
+          language="typescript"
+          value={transition.lambdaCode || ""}
+          path={`inmemory://sdcpn/transitions/${transition.id}/lambda.ts`}
+          height={340}
+          onChange={(value) => {
+            updateTransition(transition.id, (existingTransition) => {
+              existingTransition.lambdaCode = value ?? "";
+            });
+          }}
+          options={{ readOnly: isReadOnly }}
+          tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       {/* Only show Transition Results if at least one output place has a type */}
@@ -665,41 +626,24 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
               />
             )}
           </div>
-          <DisabledTooltip disabled={isReadOnly}>
-            <div
-              className={editorContainerStyle({ isReadOnly, size: "kernel" })}
-            >
-              <MonacoEditor
-                key={`kernel-${transition.inputArcs
-                  .map((a) => `${a.placeId}:${a.weight}`)
-                  .join("-")}-${transition.outputArcs
-                  .map((a) => `${a.placeId}:${a.weight}`)
-                  .join("-")}`}
-                language="typescript"
-                value={transition.transitionKernelCode || ""}
-                path={`inmemory://sdcpn/transitions/${transition.id}/transition-kernel.ts`}
-                onChange={(value) => {
-                  updateTransition(transition.id, (existingTransition) => {
-                    existingTransition.transitionKernelCode = value ?? "";
-                  });
-                }}
-                theme="vs-light"
-                options={{
-                  minimap: { enabled: false },
-                  scrollBeyondLastLine: false,
-                  fontSize: 12,
-                  lineNumbers: "off",
-                  folding: true,
-                  glyphMargin: false,
-                  lineDecorationsWidth: 0,
-                  lineNumbersMinChars: 3,
-                  padding: { top: 8, bottom: 8 },
-                  readOnly: isReadOnly,
-                  fixedOverflowWidgets: true,
-                }}
-              />
-            </div>
-          </DisabledTooltip>
+          <CodeEditor
+            key={`kernel-${transition.inputArcs
+              .map((a) => `${a.placeId}:${a.weight}`)
+              .join("-")}-${transition.outputArcs
+              .map((a) => `${a.placeId}:${a.weight}`)
+              .join("-")}`}
+            language="typescript"
+            value={transition.transitionKernelCode || ""}
+            path={`inmemory://sdcpn/transitions/${transition.id}/transition-kernel.ts`}
+            height={400}
+            onChange={(value) => {
+              updateTransition(transition.id, (existingTransition) => {
+                existingTransition.transitionKernelCode = value ?? "";
+              });
+            }}
+            options={{ readOnly: isReadOnly }}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          />
         </div>
       ) : (
         <div className={noOutputTypesBoxStyle}>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
index aa96b289689..8ade5b9d42b 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
@@ -2,7 +2,8 @@ import { css, cva } from "@hashintel/ds-helpers/css";
 import { useState } from "react";
 import { v4 as uuidv4 } from "uuid";
 
-import { DisabledTooltip } from "../../../../components/disabled-tooltip";
+import { Tooltip } from "../../../../components/tooltip";
+import { UI_MESSAGES } from "../../../../constants/ui-messages";
 import type { Color } from "../../../../core/types/sdcpn";
 import { useIsReadOnly } from "../../../../state/use-is-read-only";
 import { ColorSelect } from "./color-select";
@@ -403,7 +404,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <DisabledTooltip disabled={isDisabled}>
+        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <input
             type="text"
             value={type.name}
@@ -415,12 +416,12 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
             disabled={isDisabled}
             className={inputStyle({ isDisabled })}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       <div>
         <div className={fieldLabelStyle}>Color</div>
-        <DisabledTooltip disabled={isDisabled}>
+        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
           <ColorSelect
             value={type.displayColor}
             onChange={(color) => {
@@ -430,7 +431,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
             }}
             disabled={isDisabled}
           />
-        </DisabledTooltip>
+        </Tooltip>
       </div>
 
       {/* Dimensions Section - Editable with drag-to-reorder */}
@@ -440,7 +441,9 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
             Dimensions
             <span className={dimensionsHintStyle}>(order matters)</span>
           </div>
-          <DisabledTooltip disabled={isDisabled}>
+          <Tooltip
+            content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          >
             <button
               type="button"
               onClick={handleAddElement}
@@ -450,7 +453,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
             >
               +
             </button>
-          </DisabledTooltip>
+          </Tooltip>
         </div>
 
         {type.elements.length === 0 ? (
@@ -489,7 +492,9 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                 <div className={indexChipStyle}>{index}</div>
 
                 {/* Name input */}
-                <DisabledTooltip disabled={isDisabled}>
+                <Tooltip
+                  content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                >
                   <input
                     type="text"
                     value={element.name}
@@ -509,10 +514,12 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                     placeholder="dimension_name"
                     className={dimensionNameInputStyle({ isDisabled })}
                   />
-                </DisabledTooltip>
+                </Tooltip>
 
                 {/* Delete button */}
-                <DisabledTooltip disabled={isDisabled}>
+                <Tooltip
+                  content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                >
                   <button
                     type="button"
                     onClick={() => {
@@ -524,7 +531,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                   >
                     ×
                   </button>
-                </DisabledTooltip>
+                </Tooltip>
               </div>
             ))}
           </div>

From 9c737c24d74ab8d3d83b1cbf834a60ce4420be68 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 02:28:16 +0100
Subject: [PATCH 02/29] Fix Switch tooltip by wrapping with ark.span for proper
 event forwarding

The ArkSwitch.Root is a label element that doesn't forward all props
needed by Tooltip. Wrap the switch in an ark.span container when a
tooltip is provided to ensure event handlers are properly forwarded.

Also refactored styles to use Panda CSS data attribute patterns
(_disabled, _enabled) instead of inline conditional cursor styles.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/switch.tsx       | 80 +++++++++++--------
 1 file changed, 48 insertions(+), 32 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/switch.tsx b/libs/@hashintel/petrinaut/src/components/switch.tsx
index 2184e53ec0b..a41f3e47c6c 100644
--- a/libs/@hashintel/petrinaut/src/components/switch.tsx
+++ b/libs/@hashintel/petrinaut/src/components/switch.tsx
@@ -1,8 +1,47 @@
+import { ark } from "@ark-ui/react/factory";
 import { Switch as ArkSwitch } from "@ark-ui/react/switch";
 import { css } from "@hashintel/ds-helpers/css";
 
 import { Tooltip } from "./tooltip";
 
+const switchContainerStyle = css({
+  display: "inline-flex",
+});
+
+const controlStyle = css({
+  position: "relative",
+  display: "inline-block",
+  width: "[34px]",
+  height: "[20px]",
+  borderRadius: "[10px]",
+  transition: "[all 0.2s ease]",
+  backgroundColor: "gray.40",
+  _checked: {
+    backgroundColor: "green.40",
+  },
+  _disabled: {
+    cursor: "not-allowed",
+  },
+  _enabled: {
+    cursor: "pointer",
+  },
+});
+
+const thumbStyle = css({
+  position: "absolute",
+  top: "[3px]",
+  left: "[3px]",
+  width: "[14px]",
+  height: "[14px]",
+  borderRadius: "[7px]",
+  backgroundColor: "[white]",
+  boxShadow: "[0 2px 4px rgba(0,0,0,0.2)]",
+  transition: "[all 0.2s ease]",
+  "&[data-state='checked']": {
+    transform: "[translateX(14px)]",
+  },
+});
+
 interface SwitchProps {
   checked?: boolean;
   onCheckedChange?: (checked: boolean) => void;
@@ -24,44 +63,21 @@ export const Switch: React.FC<SwitchProps> = ({
       }}
       disabled={disabled}
     >
-      <ArkSwitch.Control
-        className={css({
-          position: "relative",
-          display: "inline-block",
-          width: "[34px]",
-          height: "[20px]",
-          borderRadius: "[10px]",
-          cursor: disabled ? "not-allowed" : "pointer",
-          transition: "[all 0.2s ease]",
-          backgroundColor: "gray.40",
-          _checked: {
-            backgroundColor: "green.40",
-          },
-        })}
-      >
-        <ArkSwitch.Thumb
-          className={css({
-            position: "absolute",
-            top: "[3px]",
-            left: "[3px]",
-            width: "[14px]",
-            height: "[14px]",
-            borderRadius: "[7px]",
-            backgroundColor: "[white]",
-            boxShadow: "[0 2px 4px rgba(0,0,0,0.2)]",
-            transition: "[all 0.2s ease]",
-            "&[data-state='checked']": {
-              transform: "[translateX(14px)]",
-            },
-          })}
-        />
+      <ArkSwitch.Control className={controlStyle}>
+        <ArkSwitch.Thumb className={thumbStyle} />
       </ArkSwitch.Control>
       <ArkSwitch.HiddenInput />
     </ArkSwitch.Root>
   );
 
   if (tooltip) {
-    return <Tooltip content={tooltip}>{switchElement}</Tooltip>;
+    // Wrap in ark.span to properly forward tooltip event handlers
+    // ArkSwitch.Root is a label element that doesn't forward all props needed by Tooltip
+    return (
+      <Tooltip content={tooltip}>
+        <ark.span className={switchContainerStyle}>{switchElement}</ark.span>
+      </Tooltip>
+    );
   }
 
   return switchElement;

From 180d28851c4e3ecf158fc35c3f48079df5f90bb0 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 11:42:33 +0100
Subject: [PATCH 03/29] Add disabled and tooltip props to SegmentGroup
 component

- Add disabled prop that passes through to ArkSegmentGroup.Root
- Add tooltip prop with ark.span wrapper for proper event forwarding
- Add disabled styling (opacity, cursor: not-allowed)
- Update transition-properties.tsx to use new props directly
  instead of external wrapper and Tooltip

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/segment-group.tsx          | 56 +++++++++++++++++--
 .../PropertiesPanel/transition-properties.tsx | 54 +++++++-----------
 2 files changed, 71 insertions(+), 39 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 1a12574fbf4..4219d01125c 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,5 +1,12 @@
+import { ark } from "@ark-ui/react/factory";
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
-import { cva } from "@hashintel/ds-helpers/css";
+import { css, cva } from "@hashintel/ds-helpers/css";
+
+import { Tooltip } from "./tooltip";
+
+const wrapperStyle = css({
+  display: "inline-flex",
+});
 
 const containerStyle = cva({
   base: {
@@ -19,9 +26,17 @@ const containerStyle = cva({
         padding: "[3px]",
       },
     },
+    isDisabled: {
+      true: {
+        opacity: "[0.6]",
+        cursor: "not-allowed",
+      },
+      false: {},
+    },
   },
   defaultVariants: {
     size: "md",
+    isDisabled: false,
   },
 });
 
@@ -55,7 +70,6 @@ const itemStyle = cva({
     flex: "1",
     fontWeight: "medium",
     textAlign: "center",
-    cursor: "pointer",
     transition: "[all 0.2s ease]",
     position: "relative",
     zIndex: 1,
@@ -77,9 +91,19 @@ const itemStyle = cva({
         padding: "[1px 8px]",
       },
     },
+    isDisabled: {
+      true: {
+        cursor: "not-allowed",
+        pointerEvents: "none",
+      },
+      false: {
+        cursor: "pointer",
+      },
+    },
   },
   defaultVariants: {
     size: "md",
+    isDisabled: false,
   },
 });
 
@@ -94,6 +118,10 @@ interface SegmentGroupProps {
   onChange: (value: string) => void;
   /** Size variant. Defaults to "md". */
   size?: "md" | "sm";
+  /** Whether the segment group is disabled. */
+  disabled?: boolean;
+  /** Tooltip to show when hovering (useful for explaining disabled state). */
+  tooltip?: string;
 }
 
 export const SegmentGroup: React.FC<SegmentGroupProps> = ({
@@ -101,23 +129,30 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   options,
   onChange,
   size = "md",
+  disabled = false,
+  tooltip,
 }) => {
-  return (
+  const segmentGroupElement = (
     <ArkSegmentGroup.Root
       value={value}
+      disabled={disabled}
       onValueChange={(details) => {
         if (details.value) {
           onChange(details.value);
         }
       }}
     >
-      <div className={containerStyle({ size })}>
+      <div className={containerStyle({ size, isDisabled: disabled })}>
         <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
         {options.map((option) => (
           <ArkSegmentGroup.Item
             key={option.value}
             value={option.value}
-            className={itemStyle({ isSelected: value === option.value, size })}
+            className={itemStyle({
+              isSelected: value === option.value,
+              size,
+              isDisabled: disabled,
+            })}
           >
             <ArkSegmentGroup.ItemText>{option.label}</ArkSegmentGroup.ItemText>
             <ArkSegmentGroup.ItemControl />
@@ -127,4 +162,15 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
       </div>
     </ArkSegmentGroup.Root>
   );
+
+  if (tooltip) {
+    // Wrap in ark.span to properly forward tooltip event handlers
+    return (
+      <Tooltip content={tooltip}>
+        <ark.span className={wrapperStyle}>{segmentGroupElement}</ark.span>
+      </Tooltip>
+    );
+  }
+
+  return segmentGroupElement;
 };
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index e4db10204e1..4ba5a86e43f 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -114,22 +114,8 @@ const arcListContainerStyle = css({
   overflow: "hidden",
 });
 
-const segmentGroupWrapperStyle = cva({
-  base: {
-    marginTop: "[8px]",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        opacity: "[0.6]",
-        pointerEvents: "none",
-      },
-      false: {
-        opacity: "[1]",
-        pointerEvents: "auto",
-      },
-    },
-  },
+const segmentGroupContainerStyle = css({
+  marginTop: "[8px]",
 });
 
 const infoBoxStyle = css({
@@ -453,24 +439,24 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
           Firing time
           <InfoIconTooltip tooltip="Define the rate at or conditions under which this will transition will fire, optionally based on each set of input tokens' data (where input tokens have types)." />
         </div>
-        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <div className={segmentGroupWrapperStyle({ isReadOnly })}>
-            <SegmentGroup
-              value={transition.lambdaType}
-              options={[
-                { value: "predicate", label: "Predicate" },
-                { value: "stochastic", label: "Stochastic Rate" },
-              ]}
-              onChange={(value) => {
-                updateTransition(transition.id, (existingTransition) => {
-                  existingTransition.lambdaType = value as
-                    | "predicate"
-                    | "stochastic";
-                });
-              }}
-            />
-          </div>
-        </Tooltip>
+        <div className={segmentGroupContainerStyle}>
+          <SegmentGroup
+            value={transition.lambdaType}
+            options={[
+              { value: "predicate", label: "Predicate" },
+              { value: "stochastic", label: "Stochastic Rate" },
+            ]}
+            onChange={(value) => {
+              updateTransition(transition.id, (existingTransition) => {
+                existingTransition.lambdaType = value as
+                  | "predicate"
+                  | "stochastic";
+              });
+            }}
+            disabled={isReadOnly}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          />
+        </div>
       </div>
 
       <div>

From 16e658ecd077226da3e0e2c251694792cf194990 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 11:45:01 +0100
Subject: [PATCH 04/29] Use ID composition for SegmentGroup tooltip to avoid
 layout-breaking wrapper

Instead of wrapping with ark.span (which breaks dimensions), use Ark UI's
ID composition pattern: the Tooltip and SegmentGroup container share an ID
via `ids={{ trigger: triggerId }}`, allowing the tooltip to attach without
wrapper elements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/segment-group.tsx          | 42 ++++++++++++++-----
 1 file changed, 32 insertions(+), 10 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 4219d01125c..79e3b127257 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,11 +1,16 @@
-import { ark } from "@ark-ui/react/factory";
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
+import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
+import { useId } from "react";
 
-import { Tooltip } from "./tooltip";
-
-const wrapperStyle = css({
-  display: "inline-flex",
+const tooltipContentStyle = css({
+  backgroundColor: "gray.90",
+  color: "gray.10",
+  borderRadius: "md.6",
+  fontSize: "[13px]",
+  zIndex: "[10000]",
+  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
+  padding: "[6px 10px]",
 });
 
 const containerStyle = cva({
@@ -132,6 +137,8 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   disabled = false,
   tooltip,
 }) => {
+  const triggerId = useId();
+
   const segmentGroupElement = (
     <ArkSegmentGroup.Root
       value={value}
@@ -142,7 +149,11 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
         }
       }}
     >
-      <div className={containerStyle({ size, isDisabled: disabled })}>
+      {/* Use the container div as the tooltip trigger via shared ID */}
+      <div
+        id={tooltip ? triggerId : undefined}
+        className={containerStyle({ size, isDisabled: disabled })}
+      >
         <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
         {options.map((option) => (
           <ArkSegmentGroup.Item
@@ -164,11 +175,22 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   );
 
   if (tooltip) {
-    // Wrap in ark.span to properly forward tooltip event handlers
+    // Use ID composition to coordinate Tooltip with the SegmentGroup container
+    // This avoids wrapper elements that break layout
     return (
-      <Tooltip content={tooltip}>
-        <ark.span className={wrapperStyle}>{segmentGroupElement}</ark.span>
-      </Tooltip>
+      <ArkTooltip.Root
+        ids={{ trigger: triggerId }}
+        openDelay={200}
+        closeDelay={0}
+        positioning={{ placement: "top" }}
+      >
+        {segmentGroupElement}
+        <ArkTooltip.Positioner>
+          <ArkTooltip.Content className={tooltipContentStyle}>
+            {tooltip}
+          </ArkTooltip.Content>
+        </ArkTooltip.Positioner>
+      </ArkTooltip.Root>
     );
   }
 

From 39448b5359c03c57f47bb7dc0d6c62010c7a38f2 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 11:48:15 +0100
Subject: [PATCH 05/29] Use display:contents wrapper for SegmentGroup tooltip

The display:contents CSS property makes the wrapper element invisible
to layout (its children are laid out as if they were direct children
of the grandparent) while still allowing it to forward tooltip events.

This fixes both the tooltip functionality and the layout.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/segment-group.tsx          | 49 +++++++------------
 1 file changed, 17 insertions(+), 32 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 79e3b127257..553d97815bc 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,16 +1,15 @@
+import { ark } from "@ark-ui/react/factory";
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
-import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
-import { useId } from "react";
 
-const tooltipContentStyle = css({
-  backgroundColor: "gray.90",
-  color: "gray.10",
-  borderRadius: "md.6",
-  fontSize: "[13px]",
-  zIndex: "[10000]",
-  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
-  padding: "[6px 10px]",
+import { Tooltip } from "./tooltip";
+
+/**
+ * Wrapper style using display:contents to be invisible to layout
+ * while still forwarding tooltip event handlers.
+ */
+const tooltipWrapperStyle = css({
+  display: "contents",
 });
 
 const containerStyle = cva({
@@ -137,8 +136,6 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   disabled = false,
   tooltip,
 }) => {
-  const triggerId = useId();
-
   const segmentGroupElement = (
     <ArkSegmentGroup.Root
       value={value}
@@ -149,11 +146,7 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
         }
       }}
     >
-      {/* Use the container div as the tooltip trigger via shared ID */}
-      <div
-        id={tooltip ? triggerId : undefined}
-        className={containerStyle({ size, isDisabled: disabled })}
-      >
+      <div className={containerStyle({ size, isDisabled: disabled })}>
         <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
         {options.map((option) => (
           <ArkSegmentGroup.Item
@@ -175,22 +168,14 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   );
 
   if (tooltip) {
-    // Use ID composition to coordinate Tooltip with the SegmentGroup container
-    // This avoids wrapper elements that break layout
+    // Use ark.span with display:contents to forward tooltip events
+    // without affecting layout
     return (
-      <ArkTooltip.Root
-        ids={{ trigger: triggerId }}
-        openDelay={200}
-        closeDelay={0}
-        positioning={{ placement: "top" }}
-      >
-        {segmentGroupElement}
-        <ArkTooltip.Positioner>
-          <ArkTooltip.Content className={tooltipContentStyle}>
-            {tooltip}
-          </ArkTooltip.Content>
-        </ArkTooltip.Positioner>
-      </ArkTooltip.Root>
+      <Tooltip content={tooltip}>
+        <ark.span className={tooltipWrapperStyle}>
+          {segmentGroupElement}
+        </ark.span>
+      </Tooltip>
     );
   }
 

From 958fb41938acd105ceb468ea593124c25d328341 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 12:58:59 +0100
Subject: [PATCH 06/29] Fix SegmentGroup tooltip using ArkTooltip.Trigger with
 asChild

Use ArkTooltip.Trigger with asChild to merge event handlers directly
with the ark.div container element. This avoids wrapper elements
while properly forwarding tooltip trigger events.

The key is that ArkTooltip.Trigger's asChild prop merges its props
(including event handlers) with the child ark.div, which can properly
receive them.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/segment-group.tsx          | 105 +++++++++++-------
 1 file changed, 64 insertions(+), 41 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 553d97815bc..06de0c280f9 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,15 +1,16 @@
 import { ark } from "@ark-ui/react/factory";
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
+import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
 
-import { Tooltip } from "./tooltip";
-
-/**
- * Wrapper style using display:contents to be invisible to layout
- * while still forwarding tooltip event handlers.
- */
-const tooltipWrapperStyle = css({
-  display: "contents",
+const tooltipContentStyle = css({
+  backgroundColor: "gray.90",
+  color: "gray.10",
+  borderRadius: "md.6",
+  fontSize: "[13px]",
+  zIndex: "[10000]",
+  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
+  padding: "[6px 10px]",
 });
 
 const containerStyle = cva({
@@ -136,7 +137,60 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
   disabled = false,
   tooltip,
 }) => {
-  const segmentGroupElement = (
+  const containerClassName = containerStyle({ size, isDisabled: disabled });
+
+  const containerContent = (
+    <>
+      <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
+      {options.map((option) => (
+        <ArkSegmentGroup.Item
+          key={option.value}
+          value={option.value}
+          className={itemStyle({
+            isSelected: value === option.value,
+            size,
+            isDisabled: disabled,
+          })}
+        >
+          <ArkSegmentGroup.ItemText>{option.label}</ArkSegmentGroup.ItemText>
+          <ArkSegmentGroup.ItemControl />
+          <ArkSegmentGroup.ItemHiddenInput />
+        </ArkSegmentGroup.Item>
+      ))}
+    </>
+  );
+
+  if (tooltip) {
+    return (
+      <ArkTooltip.Root
+        openDelay={200}
+        closeDelay={0}
+        positioning={{ placement: "top" }}
+      >
+        <ArkSegmentGroup.Root
+          value={value}
+          disabled={disabled}
+          onValueChange={(details) => {
+            if (details.value) {
+              onChange(details.value);
+            }
+          }}
+        >
+          {/* ArkTooltip.Trigger with asChild merges props with ark.div */}
+          <ArkTooltip.Trigger asChild>
+            <ark.div className={containerClassName}>{containerContent}</ark.div>
+          </ArkTooltip.Trigger>
+        </ArkSegmentGroup.Root>
+        <ArkTooltip.Positioner>
+          <ArkTooltip.Content className={tooltipContentStyle}>
+            {tooltip}
+          </ArkTooltip.Content>
+        </ArkTooltip.Positioner>
+      </ArkTooltip.Root>
+    );
+  }
+
+  return (
     <ArkSegmentGroup.Root
       value={value}
       disabled={disabled}
@@ -146,38 +200,7 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
         }
       }}
     >
-      <div className={containerStyle({ size, isDisabled: disabled })}>
-        <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
-        {options.map((option) => (
-          <ArkSegmentGroup.Item
-            key={option.value}
-            value={option.value}
-            className={itemStyle({
-              isSelected: value === option.value,
-              size,
-              isDisabled: disabled,
-            })}
-          >
-            <ArkSegmentGroup.ItemText>{option.label}</ArkSegmentGroup.ItemText>
-            <ArkSegmentGroup.ItemControl />
-            <ArkSegmentGroup.ItemHiddenInput />
-          </ArkSegmentGroup.Item>
-        ))}
-      </div>
+      <div className={containerClassName}>{containerContent}</div>
     </ArkSegmentGroup.Root>
   );
-
-  if (tooltip) {
-    // Use ark.span with display:contents to forward tooltip events
-    // without affecting layout
-    return (
-      <Tooltip content={tooltip}>
-        <ark.span className={tooltipWrapperStyle}>
-          {segmentGroupElement}
-        </ark.span>
-      </Tooltip>
-    );
-  }
-
-  return segmentGroupElement;
 };

From 1a70954b85ea718805a7be0cabc85ab572303c44 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 13:13:29 +0100
Subject: [PATCH 07/29] Disable delete buttons in properties panels during
 simulation

The delete buttons for places and transitions are now disabled when
in read-only mode (during simulation), showing the read-only tooltip.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../PropertiesPanel/place-properties.tsx      | 52 +++++++++++++------
 .../PropertiesPanel/transition-properties.tsx | 50 ++++++++++++------
 2 files changed, 68 insertions(+), 34 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index 1a23d5de963..3f38b0dfaa1 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -60,21 +60,36 @@ const headerTitleStyle = css({
   fontSize: "[16px]",
 });
 
-const deleteButtonStyle = css({
-  display: "flex",
-  alignItems: "center",
-  justifyContent: "center",
-  width: "[24px]",
-  height: "[24px]",
-  padding: "0",
-  border: "none",
-  background: "[transparent]",
-  cursor: "pointer",
-  color: "gray.60",
-  borderRadius: "md.4",
-  _hover: {
-    color: "red.60",
-    backgroundColor: "red.10",
+const deleteButtonStyle = cva({
+  base: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    width: "[24px]",
+    height: "[24px]",
+    padding: "0",
+    border: "none",
+    background: "[transparent]",
+    color: "gray.60",
+    borderRadius: "md.4",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        cursor: "not-allowed",
+        opacity: "[0.5]",
+      },
+      false: {
+        cursor: "pointer",
+        _hover: {
+          color: "red.60",
+          backgroundColor: "red.10",
+        },
+      },
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
   },
 });
 
@@ -383,7 +398,9 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
         <div>
           <div className={headerContainerStyle}>
             <div className={headerTitleStyle}>Place</div>
-            <Tooltip content="Delete">
+            <Tooltip
+              content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
+            >
               <button
                 type="button"
                 onClick={() => {
@@ -396,7 +413,8 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
                     removePlace(place.id);
                   }
                 }}
-                className={deleteButtonStyle}
+                disabled={isReadOnly}
+                className={deleteButtonStyle({ isDisabled: isReadOnly })}
               >
                 <TbTrash size={16} />
               </button>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index 4ba5a86e43f..e93c5c36ca2 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -52,21 +52,36 @@ const headerTitleStyle = css({
   fontSize: "[16px]",
 });
 
-const deleteButtonStyle = css({
-  display: "flex",
-  alignItems: "center",
-  justifyContent: "center",
-  width: "[24px]",
-  height: "[24px]",
-  padding: "0",
-  border: "none",
-  background: "[transparent]",
-  cursor: "pointer",
-  color: "gray.60",
-  borderRadius: "md.4",
-  _hover: {
-    color: "red.60",
-    backgroundColor: "red.10",
+const deleteButtonStyle = cva({
+  base: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    width: "[24px]",
+    height: "[24px]",
+    padding: "0",
+    border: "none",
+    background: "[transparent]",
+    color: "gray.60",
+    borderRadius: "md.4",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        cursor: "not-allowed",
+        opacity: "[0.5]",
+      },
+      false: {
+        cursor: "pointer",
+        _hover: {
+          color: "red.60",
+          backgroundColor: "red.10",
+        },
+      },
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
   },
 });
 
@@ -304,7 +319,7 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
       <div>
         <div className={headerContainerStyle}>
           <div className={headerTitleStyle}>Transition</div>
-          <Tooltip content="Delete">
+          <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}>
             <button
               type="button"
               onClick={() => {
@@ -317,7 +332,8 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
                   removeTransition(transition.id);
                 }
               }}
-              className={deleteButtonStyle}
+              disabled={isReadOnly}
+              className={deleteButtonStyle({ isDisabled: isReadOnly })}
             >
               <TbTrash size={16} />
             </button>

From 57f8d65c6f6821abaa8a2186ea421c9c7bcc0e5f Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 16:24:51 +0100
Subject: [PATCH 08/29] Add tooltip to Dynamics switch when no type is selected

Shows a tooltip explaining that a token type must be selected
to enable dynamics on the place.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/constants/ui-messages.ts    | 1 +
 .../Editor/panels/PropertiesPanel/place-properties.tsx    | 8 +++++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/libs/@hashintel/petrinaut/src/constants/ui-messages.ts b/libs/@hashintel/petrinaut/src/constants/ui-messages.ts
index 4123bcf09ed..fb01c021686 100644
--- a/libs/@hashintel/petrinaut/src/constants/ui-messages.ts
+++ b/libs/@hashintel/petrinaut/src/constants/ui-messages.ts
@@ -3,5 +3,6 @@
  */
 export const UI_MESSAGES = {
   AI_FEATURE_COMING_SOON: "AI generation feature coming soon",
+  DYNAMICS_REQUIRES_TYPE: "Select a token type to enable dynamics",
   READ_ONLY_MODE: "Editing disabled while in simulation mode",
 } as const;
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index 3f38b0dfaa1..e7de69c84b0 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -514,7 +514,13 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
               <Switch
                 checked={!!place.colorId && place.dynamicsEnabled}
                 disabled={isReadOnly || place.colorId === null}
-                tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                tooltip={
+                  isReadOnly
+                    ? UI_MESSAGES.READ_ONLY_MODE
+                    : place.colorId === null
+                      ? UI_MESSAGES.DYNAMICS_REQUIRES_TYPE
+                      : undefined
+                }
                 onCheckedChange={(checked) => {
                   updatePlace(place.id, (existingPlace) => {
                     existingPlace.dynamicsEnabled = checked;

From d13cd43c8c108258ec11c8b9974cfec6b01f6ca0 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 16:26:00 +0100
Subject: [PATCH 09/29] Disable Visualizer toggle during simulation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../views/Editor/panels/PropertiesPanel/place-properties.tsx    | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index e7de69c84b0..00e0357faa2 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -597,6 +597,8 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
               <div className={switchContainerStyle}>
                 <Switch
                   checked={place.visualizerCode !== undefined}
+                  disabled={isReadOnly}
+                  tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
                   onCheckedChange={(checked) => {
                     if (checked) {
                       // Turning on: use saved code if available, otherwise default

From 303c763a920c8b7dd84e9d7cfcff512a7c85d101 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 16:43:49 +0100
Subject: [PATCH 10/29] Keep editor read-only when simulation is complete

The editor should remain read-only not just during running/paused
simulations, but also when a simulation has completed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/state/use-is-read-only.ts | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts
index 09c87863f16..d911f11e673 100644
--- a/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts
+++ b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts
@@ -8,17 +8,19 @@ import { EditorContext } from "./editor-context";
  *
  * The editor is read-only when:
  * 1. The global mode is "simulate" (user has switched to simulation mode), OR
- * 2. A simulation is currently running or paused (has been initialized)
+ * 2. A simulation is currently running, paused, or complete
  *
  * When read-only, structural changes to the SDCPN (places, transitions, arcs, etc.)
- * are prevented to maintain consistency with the running simulation.
+ * are prevented to maintain consistency with the simulation.
  */
 export const useIsReadOnly = (): boolean => {
   const { globalMode } = use(EditorContext);
   const { state: simulationState } = use(SimulationContext);
 
   const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+    simulationState === "Running" ||
+    simulationState === "Paused" ||
+    simulationState === "Complete";
 
   const isReadOnly = globalMode === "simulate" || isSimulationActive;
   return isReadOnly;

From 2d7a53fd1801a46c78581084fee895950ef6c073 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Thu, 22 Jan 2026 16:53:28 +0100
Subject: [PATCH 11/29] Prevent Delete/Backspace keyboard deletion in read-only
 mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
index 1ab40da6c44..f38afb1d014 100644
--- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
+++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
@@ -307,7 +307,7 @@ export const SDCPNView: React.FC = () => {
       onKeyDown={({ key }) => {
         // Quick-and-dirty way to delete selected items with keyboard
         // with two different keys (Delete and Backspace), not possible with ReactFlow `deleteKeyCode` prop
-        if (key === "Delete" || key === "Backspace") {
+        if ((key === "Delete" || key === "Backspace") && !isReadonly) {
           setSelectedResourceId(null);
           clearSelection();
           deleteItemsByIds(selectedItemIds);

From 07273d1fd21aca5ce2c7d0d9150efc899e5d00b5 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 01:03:31 +0100
Subject: [PATCH 12/29] Use useIsReadOnly hook in SDCPN canvas view

Consolidates read-only state logic by using the shared hook instead
of duplicating the simulation state checks locally.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/views/SDCPN/sdcpn-view.tsx          | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
index f38afb1d014..82e07edde1e 100644
--- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
+++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
@@ -10,9 +10,9 @@ import {
   DEFAULT_TRANSITION_KERNEL_CODE,
   generateDefaultLambdaCode,
 } from "../../core/default-codes";
-import { SimulationContext } from "../../simulation/context";
 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 { PlaceNode } from "./components/place-node";
 import { TransitionNode } from "./components/transition-node";
@@ -74,8 +74,6 @@ export const SDCPNView: React.FC = () => {
     clearSelection,
   } = use(EditorContext);
 
-  const { state: simulationState } = use(SimulationContext);
-
   // Hook for applying node changes
   const applyNodeChanges = useApplyNodeChanges();
 
@@ -87,10 +85,9 @@ export const SDCPNView: React.FC = () => {
     reactFlowInstance?.fitView({ padding: 0.4, minZoom: 0.4, maxZoom: 1.1 });
   }, [reactFlowInstance, petriNetId]);
 
-  // Readonly if in simulate mode, simulation is running/paused, or readonly has been provided by external consumer.
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
-  const isReadonly = mode === "simulate" || isSimulationActive || readonly;
+  // Readonly if simulation mode or readonly has been provided by external consumer.
+  const isSimulationReadOnly = useIsReadOnly();
+  const isReadonly = isSimulationReadOnly || readonly;
 
   function isValidConnection(connection: Connection) {
     const sourceNode = nodes.find((node) => node.id === connection.source);

From 060eed5bd94793e578746135c71b7df79368cf83 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 01:08:53 +0100
Subject: [PATCH 13/29] Disable add/delete buttons in sidebar lists during
 read-only mode

Uses useIsReadOnly hook consistently across types, differential equations,
and parameters lists. Both add and delete buttons are now disabled with
tooltips when in read-only mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../subviews/differential-equations-list.tsx  |  92 ++++++++-------
 .../views/Editor/subviews/parameters-list.tsx |  76 ++++++------
 .../src/views/Editor/subviews/types-list.tsx  | 110 +++++++++---------
 3 files changed, 141 insertions(+), 137 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
index e9f152fc316..3e844547fb0 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
@@ -3,10 +3,12 @@ import { use } from "react";
 import { v4 as uuidv4 } from "uuid";
 
 import type { SubView } from "../../../components/sub-view/types";
+import { Tooltip } from "../../../components/tooltip";
+import { UI_MESSAGES } from "../../../constants/ui-messages";
 import { DEFAULT_DIFFERENTIAL_EQUATION_CODE } from "../../../core/default-codes";
-import { SimulationContext } from "../../../simulation/context";
 import { EditorContext } from "../../../state/editor-context";
 import { SDCPNContext } from "../../../state/sdcpn-context";
+import { useIsReadOnly } from "../../../state/use-is-read-only";
 
 const listContainerStyle = css({
   display: "flex",
@@ -120,10 +122,7 @@ const DifferentialEquationsSectionContent: React.FC = () => {
 
   const { selectedResourceId, setSelectedResourceId } = use(EditorContext);
 
-  // Check if simulation is running or paused
-  const { state: simulationState } = use(SimulationContext);
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+  const isReadOnly = useIsReadOnly();
 
   return (
     <div className={listContainerStyle}>
@@ -155,24 +154,28 @@ const DifferentialEquationsSectionContent: React.FC = () => {
             <div className={equationNameContainerStyle}>
               <span>{eq.name}</span>
             </div>
-            <button
-              type="button"
-              disabled={isSimulationActive}
-              onClick={() => {
-                if (
-                  // eslint-disable-next-line no-alert
-                  window.confirm(
-                    `Delete equation "${eq.name}"? Any places referencing this equation will have their differential equation reset.`,
-                  )
-                ) {
-                  removeDifferentialEquation(eq.id);
-                }
-              }}
-              className={deleteButtonStyle}
-              aria-label={`Delete equation ${eq.name}`}
+            <Tooltip
+              content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
             >
-              ×
-            </button>
+              <button
+                type="button"
+                disabled={isReadOnly}
+                onClick={() => {
+                  if (
+                    // eslint-disable-next-line no-alert
+                    window.confirm(
+                      `Delete equation "${eq.name}"? Any places referencing this equation will have their differential equation reset.`,
+                    )
+                  ) {
+                    removeDifferentialEquation(eq.id);
+                  }
+                }}
+                className={deleteButtonStyle}
+                aria-label={`Delete equation ${eq.name}`}
+              >
+                ×
+              </button>
+            </Tooltip>
           </div>
         );
       })}
@@ -193,31 +196,30 @@ const DifferentialEquationsSectionHeaderAction: React.FC = () => {
   } = use(SDCPNContext);
   const { setSelectedResourceId } = use(EditorContext);
 
-  // Check if simulation is running or paused
-  const { state: simulationState } = use(SimulationContext);
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+  const isReadOnly = useIsReadOnly();
 
   return (
-    <button
-      type="button"
-      disabled={isSimulationActive}
-      onClick={() => {
-        const name = `Equation ${differentialEquations.length + 1}`;
-        const id = uuidv4();
-        addDifferentialEquation({
-          id,
-          name,
-          colorId: types.length > 0 ? types[0]!.id : "",
-          code: DEFAULT_DIFFERENTIAL_EQUATION_CODE,
-        });
-        setSelectedResourceId(id);
-      }}
-      className={addButtonStyle}
-      aria-label="Add differential equation"
-    >
-      +
-    </button>
+    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+      <button
+        type="button"
+        disabled={isReadOnly}
+        onClick={() => {
+          const name = `Equation ${differentialEquations.length + 1}`;
+          const id = uuidv4();
+          addDifferentialEquation({
+            id,
+            name,
+            colorId: types.length > 0 ? types[0]!.id : "",
+            code: DEFAULT_DIFFERENTIAL_EQUATION_CODE,
+          });
+          setSelectedResourceId(id);
+        }}
+        className={addButtonStyle}
+        aria-label="Add differential equation"
+      >
+        +
+      </button>
+    </Tooltip>
   );
 };
 
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
index 7de447c256a..d447eeccdc1 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
@@ -3,9 +3,12 @@ import { use } from "react";
 import { v4 as uuidv4 } from "uuid";
 
 import type { SubView } from "../../../components/sub-view/types";
+import { Tooltip } from "../../../components/tooltip";
+import { UI_MESSAGES } from "../../../constants/ui-messages";
 import { SimulationContext } from "../../../simulation/context";
 import { EditorContext } from "../../../state/editor-context";
 import { SDCPNContext } from "../../../state/sdcpn-context";
+import { useIsReadOnly } from "../../../state/use-is-read-only";
 
 const addButtonStyle = css({
   display: "flex",
@@ -138,12 +141,9 @@ const ParametersHeaderAction: React.FC = () => {
     petriNetDefinition: { parameters },
     addParameter,
   } = use(SDCPNContext);
-  const { globalMode, setSelectedResourceId } = use(EditorContext);
-  const { state: simulationState } = use(SimulationContext);
+  const { setSelectedResourceId } = use(EditorContext);
 
-  const isSimulationMode = globalMode === "simulate";
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+  const isReadOnly = useIsReadOnly();
 
   const handleAddParameter = () => {
     const name = `param${parameters.length + 1}`;
@@ -158,21 +158,18 @@ const ParametersHeaderAction: React.FC = () => {
     setSelectedResourceId(id);
   };
 
-  // Don't show add button in simulation mode
-  if (isSimulationMode) {
-    return null;
-  }
-
   return (
-    <button
-      type="button"
-      disabled={isSimulationActive}
-      onClick={handleAddParameter}
-      className={addButtonStyle}
-      aria-label="Add parameter"
-    >
-      +
-    </button>
+    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+      <button
+        type="button"
+        disabled={isReadOnly}
+        onClick={handleAddParameter}
+        className={addButtonStyle}
+        aria-label="Add parameter"
+      >
+        +
+      </button>
+    </Tooltip>
   );
 };
 
@@ -192,14 +189,11 @@ const ParametersList: React.FC = () => {
     setParameterValue,
   } = use(SimulationContext);
 
+  const isReadOnly = useIsReadOnly();
   const isSimulationNotRun =
     globalMode === "simulate" && simulationState === "NotRun";
   const isSimulationMode = globalMode === "simulate";
 
-  // Check if simulation is running or paused
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
-
   return (
     <div>
       <div className={listContainerStyle}>
@@ -250,22 +244,28 @@ const ParametersList: React.FC = () => {
                     className={inputStyle}
                   />
                 ) : (
-                  <button
-                    type="button"
-                    disabled={isSimulationActive}
-                    onClick={() => {
-                      if (
-                        // eslint-disable-next-line no-alert
-                        window.confirm(`Delete parameter "${param.name}"?`)
-                      ) {
-                        removeParameter(param.id);
-                      }
-                    }}
-                    className={deleteButtonStyle}
-                    aria-label={`Delete ${param.name}`}
+                  <Tooltip
+                    content={
+                      isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined
+                    }
                   >
-                    ×
-                  </button>
+                    <button
+                      type="button"
+                      disabled={isReadOnly}
+                      onClick={() => {
+                        if (
+                          // eslint-disable-next-line no-alert
+                          window.confirm(`Delete parameter "${param.name}"?`)
+                        ) {
+                          removeParameter(param.id);
+                        }
+                      }}
+                      className={deleteButtonStyle}
+                      aria-label={`Delete ${param.name}`}
+                    >
+                      ×
+                    </button>
+                  </Tooltip>
                 )}
               </div>
             </div>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
index 6adf4dd58b4..bcb59b9b1eb 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
@@ -2,9 +2,11 @@ import { css, cva } from "@hashintel/ds-helpers/css";
 import { use } from "react";
 
 import type { SubView } from "../../../components/sub-view/types";
-import { SimulationContext } from "../../../simulation/context";
+import { Tooltip } from "../../../components/tooltip";
+import { UI_MESSAGES } from "../../../constants/ui-messages";
 import { EditorContext } from "../../../state/editor-context";
 import { SDCPNContext } from "../../../state/sdcpn-context";
+import { useIsReadOnly } from "../../../state/use-is-read-only";
 
 const listContainerStyle = css({
   display: "flex",
@@ -171,10 +173,7 @@ const TypesSectionContent: React.FC = () => {
 
   const { selectedResourceId, setSelectedResourceId } = use(EditorContext);
 
-  // Check if simulation is running or paused
-  const { state: simulationState } = use(SimulationContext);
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+  const isReadOnly = useIsReadOnly();
 
   return (
     <div className={listContainerStyle}>
@@ -208,24 +207,28 @@ const TypesSectionContent: React.FC = () => {
               style={{ backgroundColor: type.displayColor }}
             />
             <span className={typeNameStyle}>{type.name}</span>
-            <button
-              type="button"
-              disabled={isSimulationActive}
-              onClick={() => {
-                if (
-                  // eslint-disable-next-line no-alert
-                  window.confirm(
-                    `Delete token type "${type.name}"? All places using this type will have their type set to null.`,
-                  )
-                ) {
-                  removeType(type.id);
-                }
-              }}
-              className={deleteButtonStyle}
-              aria-label={`Delete token type ${type.name}`}
+            <Tooltip
+              content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
             >
-              ×
-            </button>
+              <button
+                type="button"
+                disabled={isReadOnly}
+                onClick={() => {
+                  if (
+                    // eslint-disable-next-line no-alert
+                    window.confirm(
+                      `Delete token type "${type.name}"? All places using this type will have their type set to null.`,
+                    )
+                  ) {
+                    removeType(type.id);
+                  }
+                }}
+                className={deleteButtonStyle}
+                aria-label={`Delete token type ${type.name}`}
+              >
+                ×
+              </button>
+            </Tooltip>
           </div>
         );
       })}
@@ -245,41 +248,40 @@ const TypesSectionHeaderAction: React.FC = () => {
     addType,
   } = use(SDCPNContext);
 
-  // Check if simulation is running or paused
-  const { state: simulationState } = use(SimulationContext);
-  const isSimulationActive =
-    simulationState === "Running" || simulationState === "Paused";
+  const isReadOnly = useIsReadOnly();
 
   return (
-    <button
-      type="button"
-      disabled={isSimulationActive}
-      onClick={() => {
-        const existingColors = types.map((type) => type.displayColor);
-        const existingNames = types.map((type) => type.name);
-        const nextNumber = getNextTypeNumber(existingNames);
-        const nextColor = getNextAvailableColor(existingColors);
+    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+      <button
+        type="button"
+        disabled={isReadOnly}
+        onClick={() => {
+          const existingColors = types.map((type) => type.displayColor);
+          const existingNames = types.map((type) => type.name);
+          const nextNumber = getNextTypeNumber(existingNames);
+          const nextColor = getNextAvailableColor(existingColors);
 
-        const newType = {
-          id: `type__${Date.now()}`,
-          name: `Type ${nextNumber}`,
-          iconSlug: "circle",
-          displayColor: nextColor,
-          elements: [
-            {
-              elementId: `element__${Date.now()}`,
-              name: "dimension_1",
-              type: "real" as const,
-            },
-          ],
-        };
-        addType(newType);
-      }}
-      className={addButtonStyle}
-      aria-label="Add token type"
-    >
-      +
-    </button>
+          const newType = {
+            id: `type__${Date.now()}`,
+            name: `Type ${nextNumber}`,
+            iconSlug: "circle",
+            displayColor: nextColor,
+            elements: [
+              {
+                elementId: `element__${Date.now()}`,
+                name: "dimension_1",
+                type: "real" as const,
+              },
+            ],
+          };
+          addType(newType);
+        }}
+        className={addButtonStyle}
+        aria-label="Add token type"
+      >
+        +
+      </button>
+    </Tooltip>
   );
 };
 

From 6108b91c057f498eb7c20ef55f4e795fdd599ab5 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 01:36:07 +0100
Subject: [PATCH 14/29] Add changeset for read-only tooltips fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .changeset/fix-readonly-tooltips.md | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .changeset/fix-readonly-tooltips.md

diff --git a/.changeset/fix-readonly-tooltips.md b/.changeset/fix-readonly-tooltips.md
new file mode 100644
index 00000000000..b84fd5e33d0
--- /dev/null
+++ b/.changeset/fix-readonly-tooltips.md
@@ -0,0 +1,5 @@
+---
+"@hashintel/petrinaut": patch
+---
+
+Fix read-only tooltips to always show during simulation mode

From e8c28b7f9f23a53c55064db6935c1ecaac9f3020 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:30:56 +0100
Subject: [PATCH 15/29] Fix CodeEditor overlay blocking scroll in read-only
 mode

Move cursor style to container and remove tooltip overlay that was
blocking mouse interactions including scroll wheel events.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/components/code-editor.tsx | 9 +--------
 1 file changed, 1 insertion(+), 8 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
index 8f8e636f03e..6fa135c4b64 100644
--- a/libs/@hashintel/petrinaut/src/components/code-editor.tsx
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -15,19 +15,13 @@ const containerStyle = cva({
     isReadOnly: {
       true: {
         filter: "[grayscale(20%) brightness(98%)]",
+        cursor: "not-allowed",
       },
       false: {},
     },
   },
 });
 
-const tooltipOverlayStyle = css({
-  position: "absolute",
-  inset: "[0]",
-  zIndex: "[1]",
-  cursor: "not-allowed",
-});
-
 type CodeEditorProps = Omit<EditorProps, "theme"> & {
   tooltip?: string;
 };
@@ -68,7 +62,6 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
         options={editorOptions}
         {...props}
       />
-      {tooltip && <div className={tooltipOverlayStyle} />}
     </div>
   );
 

From 1b95fc345369c48fbc1896b376e1c7bb1e97b044 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:32:05 +0100
Subject: [PATCH 16/29] Fix SegmentGroup tooltip nesting structure

Move ArkTooltip.Trigger to wrap the entire SegmentGroup.Root instead
of being nested inside it. This ensures proper component hierarchy
and context distribution.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/segment-group.tsx          | 29 ++++++++++---------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 06de0c280f9..ee12936eea5 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -167,20 +167,21 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
         closeDelay={0}
         positioning={{ placement: "top" }}
       >
-        <ArkSegmentGroup.Root
-          value={value}
-          disabled={disabled}
-          onValueChange={(details) => {
-            if (details.value) {
-              onChange(details.value);
-            }
-          }}
-        >
-          {/* ArkTooltip.Trigger with asChild merges props with ark.div */}
-          <ArkTooltip.Trigger asChild>
-            <ark.div className={containerClassName}>{containerContent}</ark.div>
-          </ArkTooltip.Trigger>
-        </ArkSegmentGroup.Root>
+        <ArkTooltip.Trigger asChild>
+          <ark.div>
+            <ArkSegmentGroup.Root
+              value={value}
+              disabled={disabled}
+              onValueChange={(details) => {
+                if (details.value) {
+                  onChange(details.value);
+                }
+              }}
+            >
+              <div className={containerClassName}>{containerContent}</div>
+            </ArkSegmentGroup.Root>
+          </ark.div>
+        </ArkTooltip.Trigger>
         <ArkTooltip.Positioner>
           <ArkTooltip.Content className={tooltipContentStyle}>
             {tooltip}

From 4b5d66355df2c5162a74994e998d83dd1d27eac5 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:34:09 +0100
Subject: [PATCH 17/29] Fix Tooltip to work on disabled elements

Add a wrapper span around the tooltip trigger children to ensure
tooltips work on disabled native elements (like buttons). Disabled
elements don't receive pointer events, but the wrapper span does.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/tooltip.tsx         | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index 61abc071929..d71ca322485 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -22,6 +22,17 @@ interface TooltipProps {
   children: ReactNode;
 }
 
+const triggerWrapperStyle = css({
+  display: "inline-flex",
+  width: "[100%]",
+});
+
+/**
+ * Tooltip component that wraps children and shows a tooltip on hover.
+ *
+ * Uses a wrapper span to ensure tooltips work on disabled elements,
+ * since disabled elements don't receive pointer events.
+ */
 export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
   if (!content) {
     return children;
@@ -33,7 +44,10 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
       closeDelay={0}
       positioning={{ placement: "top" }}
     >
-      <ArkTooltip.Trigger asChild>{children}</ArkTooltip.Trigger>
+      {/* Wrapper span ensures tooltip works on disabled elements */}
+      <ArkTooltip.Trigger asChild>
+        <span className={triggerWrapperStyle}>{children}</span>
+      </ArkTooltip.Trigger>
       <ArkTooltip.Positioner>
         <ArkTooltip.Content className={tooltipContentStyle}>
           {content}

From 5d3dd1945a29ee6719ae74500af971cc91bc2655 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:43:37 +0100
Subject: [PATCH 18/29] Fix Tooltip wrapper to use flex instead of inline-flex

Using inline-flex caused block-level children like CodeEditor to lose
their width. Using flex ensures the wrapper takes full width while
still properly wrapping both block and inline elements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/components/tooltip.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index d71ca322485..66f72cbf894 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -23,8 +23,7 @@ interface TooltipProps {
 }
 
 const triggerWrapperStyle = css({
-  display: "inline-flex",
-  width: "[100%]",
+  display: "flex",
 });
 
 /**

From 24e335766c9f65cbca8c02e6769060600e274579 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:50:00 +0100
Subject: [PATCH 19/29] Add width: 100% to Tooltip wrapper for block elements

Ensure the flex wrapper takes full width of its parent container
so block-level children like CodeEditor don't collapse.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/components/tooltip.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index 66f72cbf894..e2d7b401b71 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -24,6 +24,7 @@ interface TooltipProps {
 
 const triggerWrapperStyle = css({
   display: "flex",
+  width: "[100%]",
 });
 
 /**

From e0e80321c5851ba7ac4bd7a8e36b28fd836a3cd7 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Fri, 23 Jan 2026 10:51:05 +0100
Subject: [PATCH 20/29] Use div instead of span for Tooltip wrapper

Block-level div elements properly contain block-level children like
CodeEditor. The span element wasn't correctly handling the width
inheritance for Monaco Editor containers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/components/tooltip.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index e2d7b401b71..af9d6be098b 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -44,9 +44,9 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
       closeDelay={0}
       positioning={{ placement: "top" }}
     >
-      {/* Wrapper span ensures tooltip works on disabled elements */}
+      {/* Wrapper div ensures tooltip works on disabled elements */}
       <ArkTooltip.Trigger asChild>
-        <span className={triggerWrapperStyle}>{children}</span>
+        <div className={triggerWrapperStyle}>{children}</div>
       </ArkTooltip.Trigger>
       <ArkTooltip.Positioner>
         <ArkTooltip.Content className={tooltipContentStyle}>

From 83b22c862fc6011d0031a3b7c76ffc59147d7aad Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Sat, 24 Jan 2026 13:37:18 +0100
Subject: [PATCH 21/29] Fix Tooltip wrapper to preserve input dimensions

The previous fix using flex with width: 100% was breaking the dimensions
of input elements. This change uses inline-flex instead, which preserves
the natural dimensions of input elements while still enabling tooltips
on disabled elements.

The inline-flex display mode ensures that:
1. Input elements maintain their intended width
2. Tooltips still work on disabled elements
3. The wrapper doesn't force unwanted width constraints
4. Alignment is preserved with align-items: center

Fixes issue with Tooltip wrapper breaking dimensions of inputs.
---
 libs/@hashintel/petrinaut/src/components/tooltip.tsx | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index af9d6be098b..31f3661a5ca 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -23,15 +23,15 @@ interface TooltipProps {
 }
 
 const triggerWrapperStyle = css({
-  display: "flex",
-  width: "[100%]",
+  display: "inline-flex",
+  alignItems: "center",
 });
 
 /**
  * Tooltip component that wraps children and shows a tooltip on hover.
  *
- * Uses a wrapper span to ensure tooltips work on disabled elements,
- * since disabled elements don't receive pointer events.
+ * Uses a wrapper div with inline-flex to ensure tooltips work on disabled elements
+ * and preserve the natural dimensions of input elements.
  */
 export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
   if (!content) {
@@ -44,7 +44,7 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
       closeDelay={0}
       positioning={{ placement: "top" }}
     >
-      {/* Wrapper div ensures tooltip works on disabled elements */}
+      {/* Wrapper div with inline-flex preserves input dimensions while enabling tooltips on disabled elements */}
       <ArkTooltip.Trigger asChild>
         <div className={triggerWrapperStyle}>{children}</div>
       </ArkTooltip.Trigger>

From 3620c95f97e62c0cc641f7216707198c11534990 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 13:03:10 +0100
Subject: [PATCH 22/29] Refactor Tooltip component with display variants and
 fix readonly guards

- Tooltip now accepts a `display` prop ("block" | "inline") to control
  wrapper element behavior, preventing layout issues
- Block mode (default): For full-width elements like inputs/selects
- Inline mode: For buttons and inline elements in flex containers
- Simplified SegmentGroup and Switch components to use shared Tooltip
- CodeEditor now shows tooltip on edit attempts in readonly mode via
  Monaco's onDidAttemptReadOnlyEdit event
- Added missing `display="inline"` to button tooltips throughout the app

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/code-editor.tsx  | 68 ++++++++++++++++++-
 .../src/components/segment-group.tsx          | 57 ++++------------
 .../petrinaut/src/components/switch.tsx       | 11 +--
 .../petrinaut/src/components/tooltip.tsx      | 47 ++++++++++---
 .../components/BottomBar/toolbar-button.tsx   |  2 +-
 .../views/Editor/components/mode-selector.tsx |  1 +
 .../differential-equation-properties.tsx      |  6 +-
 .../PropertiesPanel/place-properties.tsx      |  6 +-
 .../PropertiesPanel/transition-properties.tsx | 10 ++-
 .../PropertiesPanel/type-properties.tsx       |  2 +
 .../subviews/differential-equations-list.tsx  |  6 +-
 .../views/Editor/subviews/parameters-list.tsx |  6 +-
 .../src/views/Editor/subviews/types-list.tsx  |  6 +-
 13 files changed, 154 insertions(+), 74 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
index 6fa135c4b64..7df2468de6a 100644
--- a/libs/@hashintel/petrinaut/src/components/code-editor.tsx
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -1,6 +1,9 @@
+import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
-import type { EditorProps } from "@monaco-editor/react";
+import type { EditorProps, Monaco } from "@monaco-editor/react";
 import MonacoEditor from "@monaco-editor/react";
+import type { editor } from "monaco-editor";
+import { useCallback, useRef, useState } from "react";
 
 import { Tooltip } from "./tooltip";
 
@@ -22,6 +25,16 @@ const containerStyle = cva({
   },
 });
 
+const tooltipContentStyle = css({
+  backgroundColor: "gray.90",
+  color: "gray.10",
+  borderRadius: "md.6",
+  fontSize: "[13px]",
+  zIndex: "[10000]",
+  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
+  padding: "[6px 10px]",
+});
+
 type CodeEditorProps = Omit<EditorProps, "theme"> & {
   tooltip?: string;
 };
@@ -30,15 +43,43 @@ type CodeEditorProps = Omit<EditorProps, "theme"> & {
  * Code editor component that wraps Monaco Editor.
  *
  * @param tooltip - Optional tooltip to show when hovering over the editor.
- *                  When provided, the editor becomes non-interactive (for read-only mode explanations).
+ *                  In read-only mode, the tooltip also appears when attempting to edit.
  */
 export const CodeEditor: React.FC<CodeEditorProps> = ({
   tooltip,
   options,
   height,
+  onMount,
   ...props
 }) => {
   const isReadOnly = options?.readOnly === true;
+  const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false);
+  const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  const handleMount = useCallback(
+    (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
+      if (isReadOnly && tooltip) {
+        editorInstance.onDidAttemptReadOnlyEdit(() => {
+          // Clear any existing timeout
+          if (hideTimeoutRef.current) {
+            clearTimeout(hideTimeoutRef.current);
+          }
+
+          // Show tooltip
+          setShowReadOnlyTooltip(true);
+
+          // Hide after 2 seconds
+          hideTimeoutRef.current = setTimeout(() => {
+            setShowReadOnlyTooltip(false);
+          }, 2000);
+        });
+      }
+
+      // Call the original onMount if provided
+      onMount?.(editorInstance, monaco);
+    },
+    [isReadOnly, tooltip, onMount],
+  );
 
   const editorOptions: EditorProps["options"] = {
     minimap: { enabled: false },
@@ -60,11 +101,34 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
         theme="vs-light"
         height="100%"
         options={editorOptions}
+        onMount={handleMount}
         {...props}
       />
     </div>
   );
 
+  // In read-only mode with tooltip, use controlled tooltip that shows on edit attempts
+  if (isReadOnly && tooltip) {
+    return (
+      <ArkTooltip.Root
+        open={showReadOnlyTooltip}
+        openDelay={200}
+        closeDelay={0}
+        positioning={{ placement: "top" }}
+      >
+        <ArkTooltip.Trigger asChild>
+          <div>{editor}</div>
+        </ArkTooltip.Trigger>
+        <ArkTooltip.Positioner>
+          <ArkTooltip.Content className={tooltipContentStyle}>
+            {tooltip}
+          </ArkTooltip.Content>
+        </ArkTooltip.Positioner>
+      </ArkTooltip.Root>
+    );
+  }
+
+  // Regular tooltip for non-read-only mode (if tooltip is provided)
   if (tooltip) {
     return <Tooltip content={tooltip}>{editor}</Tooltip>;
   }
diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index ee12936eea5..58514660a8d 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,17 +1,7 @@
-import { ark } from "@ark-ui/react/factory";
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
-import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { cva } from "@hashintel/ds-helpers/css";
 
-const tooltipContentStyle = css({
-  backgroundColor: "gray.90",
-  color: "gray.10",
-  borderRadius: "md.6",
-  fontSize: "[13px]",
-  zIndex: "[10000]",
-  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
-  padding: "[6px 10px]",
-});
+import { Tooltip } from "./tooltip";
 
 const containerStyle = cva({
   base: {
@@ -160,38 +150,7 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
     </>
   );
 
-  if (tooltip) {
-    return (
-      <ArkTooltip.Root
-        openDelay={200}
-        closeDelay={0}
-        positioning={{ placement: "top" }}
-      >
-        <ArkTooltip.Trigger asChild>
-          <ark.div>
-            <ArkSegmentGroup.Root
-              value={value}
-              disabled={disabled}
-              onValueChange={(details) => {
-                if (details.value) {
-                  onChange(details.value);
-                }
-              }}
-            >
-              <div className={containerClassName}>{containerContent}</div>
-            </ArkSegmentGroup.Root>
-          </ark.div>
-        </ArkTooltip.Trigger>
-        <ArkTooltip.Positioner>
-          <ArkTooltip.Content className={tooltipContentStyle}>
-            {tooltip}
-          </ArkTooltip.Content>
-        </ArkTooltip.Positioner>
-      </ArkTooltip.Root>
-    );
-  }
-
-  return (
+  const segmentGroup = (
     <ArkSegmentGroup.Root
       value={value}
       disabled={disabled}
@@ -204,4 +163,14 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
       <div className={containerClassName}>{containerContent}</div>
     </ArkSegmentGroup.Root>
   );
+
+  if (tooltip) {
+    return (
+      <Tooltip content={tooltip} display="block">
+        {segmentGroup}
+      </Tooltip>
+    );
+  }
+
+  return segmentGroup;
 };
diff --git a/libs/@hashintel/petrinaut/src/components/switch.tsx b/libs/@hashintel/petrinaut/src/components/switch.tsx
index a41f3e47c6c..9fef184fd7f 100644
--- a/libs/@hashintel/petrinaut/src/components/switch.tsx
+++ b/libs/@hashintel/petrinaut/src/components/switch.tsx
@@ -1,13 +1,8 @@
-import { ark } from "@ark-ui/react/factory";
 import { Switch as ArkSwitch } from "@ark-ui/react/switch";
 import { css } from "@hashintel/ds-helpers/css";
 
 import { Tooltip } from "./tooltip";
 
-const switchContainerStyle = css({
-  display: "inline-flex",
-});
-
 const controlStyle = css({
   position: "relative",
   display: "inline-block",
@@ -71,11 +66,9 @@ export const Switch: React.FC<SwitchProps> = ({
   );
 
   if (tooltip) {
-    // Wrap in ark.span to properly forward tooltip event handlers
-    // ArkSwitch.Root is a label element that doesn't forward all props needed by Tooltip
     return (
-      <Tooltip content={tooltip}>
-        <ark.span className={switchContainerStyle}>{switchElement}</ark.span>
+      <Tooltip content={tooltip} display="inline">
+        {switchElement}
       </Tooltip>
     );
   }
diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index 31f3661a5ca..2ebabc8e425 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -1,5 +1,6 @@
+import { ark } from "@ark-ui/react/factory";
 import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
-import { css } from "@hashintel/ds-helpers/css";
+import { css, cva } from "@hashintel/ds-helpers/css";
 import type { SvgIconProps } from "@mui/material";
 import { SvgIcon, Tooltip as MuiTooltip } from "@mui/material";
 import type { FunctionComponent, ReactNode } from "react";
@@ -14,26 +15,49 @@ const tooltipContentStyle = css({
   padding: "[6px 10px]",
 });
 
+const triggerWrapperStyle = cva({
+  variants: {
+    display: {
+      /** For block-level elements like inputs, selects - takes full width */
+      block: {
+        display: "block",
+      },
+      /** For inline elements like buttons in flex containers */
+      inline: {
+        display: "inline-block",
+      },
+    },
+  },
+  defaultVariants: {
+    display: "block",
+  },
+});
+
 interface TooltipProps {
   /**
    * The tooltip content. When empty/undefined, children are rendered without tooltip wrapper.
    */
   content?: string;
   children: ReactNode;
+  /**
+   * Display mode for the wrapper element.
+   * - "block": For full-width elements like inputs/selects (default)
+   * - "inline": For inline elements like buttons in flex containers
+   */
+  display?: "block" | "inline";
 }
 
-const triggerWrapperStyle = css({
-  display: "inline-flex",
-  alignItems: "center",
-});
-
 /**
  * Tooltip component that wraps children and shows a tooltip on hover.
  *
- * Uses a wrapper div with inline-flex to ensure tooltips work on disabled elements
- * and preserve the natural dimensions of input elements.
+ * Uses a wrapper element to capture pointer events, enabling tooltips on disabled elements.
+ * Set `display="inline"` when wrapping inline elements like buttons.
  */
-export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
+export const Tooltip: React.FC<TooltipProps> = ({
+  content,
+  children,
+  display = "block",
+}) => {
   if (!content) {
     return children;
   }
@@ -44,9 +68,10 @@ export const Tooltip: React.FC<TooltipProps> = ({ content, children }) => {
       closeDelay={0}
       positioning={{ placement: "top" }}
     >
-      {/* Wrapper div with inline-flex preserves input dimensions while enabling tooltips on disabled elements */}
       <ArkTooltip.Trigger asChild>
-        <div className={triggerWrapperStyle}>{children}</div>
+        <ark.span className={triggerWrapperStyle({ display })}>
+          {children}
+        </ark.span>
       </ArkTooltip.Trigger>
       <ArkTooltip.Positioner>
         <ArkTooltip.Content className={tooltipContentStyle}>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx
index 87e7a7a0917..c9c78f0b1c0 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx
@@ -87,7 +87,7 @@ export const ToolbarButton: React.FC<ToolbarButtonProps> = ({
   };
 
   return (
-    <Tooltip content={tooltip}>
+    <Tooltip content={tooltip} display="inline">
       <button
         type="button"
         onClick={onClick}
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx
index 3bfe0e578a1..f04d620a8e6 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx
@@ -137,6 +137,7 @@ export const ModeSelector: React.FC<ModeSelectorProps> = ({
                 <Tooltip
                   key={option.value}
                   content="Simulate tab is disabled temporarily. Simulation is available in Edit Mode."
+                  display="inline"
                 >
                   <span>{item}</span>
                 </Tooltip>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
index c88e80b57b8..19fa17f4500 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
@@ -332,6 +332,7 @@ export const DifferentialEquationProperties: React.FC<
         <div style={{ position: "relative" }}>
           <Tooltip
             content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+            display="inline"
           >
             <button
               type="button"
@@ -482,7 +483,10 @@ export const DifferentialEquationProperties: React.FC<
                 {
                   id: "generate-ai",
                   label: (
-                    <Tooltip content={UI_MESSAGES.AI_FEATURE_COMING_SOON}>
+                    <Tooltip
+                      content={UI_MESSAGES.AI_FEATURE_COMING_SOON}
+                      display="inline"
+                    >
                       <div className={aiMenuItemStyle}>
                         <TbSparkles className={aiIconStyle} />
                         Generate with AI
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index 00e0357faa2..a6d3e1e46b9 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -400,6 +400,7 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
             <div className={headerTitleStyle}>Place</div>
             <Tooltip
               content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
+              display="inline"
             >
               <button
                 type="button"
@@ -657,7 +658,10 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
                   {
                     id: "generate-ai",
                     label: (
-                      <Tooltip content={UI_MESSAGES.AI_FEATURE_COMING_SOON}>
+                      <Tooltip
+                        content={UI_MESSAGES.AI_FEATURE_COMING_SOON}
+                        display="inline"
+                      >
                         <div className={aiMenuItemStyle}>
                           <TbSparkles className={aiIconStyle} />
                           Generate with AI
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index e93c5c36ca2..b686fb6eee3 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -319,7 +319,10 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
       <div>
         <div className={headerContainerStyle}>
           <div className={headerTitleStyle}>Transition</div>
-          <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}>
+          <Tooltip
+            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
+            display="inline"
+          >
             <button
               type="button"
               onClick={() => {
@@ -512,7 +515,10 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
                 {
                   id: "generate-ai",
                   label: (
-                    <Tooltip content={UI_MESSAGES.AI_FEATURE_COMING_SOON}>
+                    <Tooltip
+                      content={UI_MESSAGES.AI_FEATURE_COMING_SOON}
+                      display="inline"
+                    >
                       <div className={aiMenuItemStyle}>
                         <TbSparkles className={aiIconStyle} />
                         Generate with AI
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
index 8ade5b9d42b..b05182a5d4e 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
@@ -443,6 +443,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
           </div>
           <Tooltip
             content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+            display="inline"
           >
             <button
               type="button"
@@ -519,6 +520,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                 {/* Delete button */}
                 <Tooltip
                   content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                  display="inline"
                 >
                   <button
                     type="button"
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
index 3e844547fb0..f6963007cd9 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx
@@ -156,6 +156,7 @@ const DifferentialEquationsSectionContent: React.FC = () => {
             </div>
             <Tooltip
               content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+              display="inline"
             >
               <button
                 type="button"
@@ -199,7 +200,10 @@ const DifferentialEquationsSectionHeaderAction: React.FC = () => {
   const isReadOnly = useIsReadOnly();
 
   return (
-    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+    <Tooltip
+      content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+      display="inline"
+    >
       <button
         type="button"
         disabled={isReadOnly}
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
index d447eeccdc1..1fbf6f543eb 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx
@@ -159,7 +159,10 @@ const ParametersHeaderAction: React.FC = () => {
   };
 
   return (
-    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+    <Tooltip
+      content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+      display="inline"
+    >
       <button
         type="button"
         disabled={isReadOnly}
@@ -248,6 +251,7 @@ const ParametersList: React.FC = () => {
                     content={
                       isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined
                     }
+                    display="inline"
                   >
                     <button
                       type="button"
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
index bcb59b9b1eb..a53d0046f1e 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx
@@ -209,6 +209,7 @@ const TypesSectionContent: React.FC = () => {
             <span className={typeNameStyle}>{type.name}</span>
             <Tooltip
               content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+              display="inline"
             >
               <button
                 type="button"
@@ -251,7 +252,10 @@ const TypesSectionHeaderAction: React.FC = () => {
   const isReadOnly = useIsReadOnly();
 
   return (
-    <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
+    <Tooltip
+      content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+      display="inline"
+    >
       <button
         type="button"
         disabled={isReadOnly}

From 401ea89ee2ea3d16211b444dd056f00bc936b405 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 14:01:55 +0100
Subject: [PATCH 23/29] Remove unused `mode` variable from SDCPNView

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
index 82e07edde1e..f0f7fb4fdb8 100644
--- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
+++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx
@@ -65,7 +65,6 @@ export const SDCPNView: React.FC = () => {
   } = use(SDCPNContext);
 
   const {
-    globalMode: mode,
     editionMode,
     setEditionMode,
     selectedItemIds,

From 1cc91a8312edf2def8c90503ab31dc4b0425ca8f Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 16:48:35 +0100
Subject: [PATCH 24/29] Add withTooltip HoC to reduce tooltip boilerplate in
 components

Introduces a reusable `withTooltip` higher-order component that adds `tooltip` and
`tooltipDisplay` props to any component. Refactors SegmentGroup and Switch to use
this HoC instead of inline tooltip handling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../src/components/hoc/with-tooltip.tsx       | 63 +++++++++++++++++++
 .../src/components/segment-group.tsx          | 61 +++++++-----------
 .../petrinaut/src/components/switch.tsx       | 46 +++++---------
 3 files changed, 103 insertions(+), 67 deletions(-)
 create mode 100644 libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx

diff --git a/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx
new file mode 100644
index 00000000000..7ef7908a513
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx
@@ -0,0 +1,63 @@
+import type { ComponentType } from "react";
+
+import { Tooltip } from "../tooltip";
+
+interface WithTooltipProps {
+  /** Tooltip to show when hovering (useful for explaining disabled state). */
+  tooltip?: string;
+  /**
+   * Display mode for the tooltip wrapper element.
+   * - "block": For full-width elements like inputs/selects (default)
+   * - "inline": For inline elements like buttons in flex containers
+   */
+  tooltipDisplay?: "block" | "inline";
+}
+
+/**
+ * Higher-Order Component that adds tooltip support to any component.
+ *
+ * When `tooltip` prop is provided, the component is wrapped with a Tooltip.
+ * When `tooltip` is undefined/empty, the component renders unwrapped.
+ *
+ * @param Component - The component to wrap
+ * @param defaultDisplay - Default display mode for the tooltip wrapper ("block" | "inline")
+ *
+ * @example
+ * ```tsx
+ * const MyButtonBase: React.FC<{ onClick: () => void }> = ({ onClick }) => (
+ *   <button onClick={onClick}>Click me</button>
+ * );
+ *
+ * export const MyButton = withTooltip(MyButtonBase, "inline");
+ *
+ * // Usage:
+ * <MyButton onClick={handleClick} tooltip="Click to submit" />
+ * ```
+ */
+export function withTooltip<P extends object>(
+  Component: ComponentType<P>,
+  defaultDisplay: "block" | "inline" = "block",
+): ComponentType<P & WithTooltipProps> {
+  const WrappedComponent: React.FC<P & WithTooltipProps> = ({
+    tooltip,
+    tooltipDisplay = defaultDisplay,
+    ...props
+  }) => {
+    const element = <Component {...(props as P)} />;
+
+    if (!tooltip) {
+      return element;
+    }
+
+    return (
+      <Tooltip content={tooltip} display={tooltipDisplay}>
+        {element}
+      </Tooltip>
+    );
+  };
+
+  // Set display name for debugging
+  WrappedComponent.displayName = `withTooltip(${Component.displayName ?? Component.name})`;
+
+  return WrappedComponent;
+}
diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
index 58514660a8d..96918c7d9eb 100644
--- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx
+++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx
@@ -1,7 +1,7 @@
 import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group";
 import { cva } from "@hashintel/ds-helpers/css";
 
-import { Tooltip } from "./tooltip";
+import { withTooltip } from "./hoc/with-tooltip";
 
 const containerStyle = cva({
   base: {
@@ -115,42 +115,18 @@ interface SegmentGroupProps {
   size?: "md" | "sm";
   /** Whether the segment group is disabled. */
   disabled?: boolean;
-  /** Tooltip to show when hovering (useful for explaining disabled state). */
-  tooltip?: string;
 }
 
-export const SegmentGroup: React.FC<SegmentGroupProps> = ({
+const SegmentGroupBase: React.FC<SegmentGroupProps> = ({
   value,
   options,
   onChange,
   size = "md",
   disabled = false,
-  tooltip,
 }) => {
   const containerClassName = containerStyle({ size, isDisabled: disabled });
 
-  const containerContent = (
-    <>
-      <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
-      {options.map((option) => (
-        <ArkSegmentGroup.Item
-          key={option.value}
-          value={option.value}
-          className={itemStyle({
-            isSelected: value === option.value,
-            size,
-            isDisabled: disabled,
-          })}
-        >
-          <ArkSegmentGroup.ItemText>{option.label}</ArkSegmentGroup.ItemText>
-          <ArkSegmentGroup.ItemControl />
-          <ArkSegmentGroup.ItemHiddenInput />
-        </ArkSegmentGroup.Item>
-      ))}
-    </>
-  );
-
-  const segmentGroup = (
+  return (
     <ArkSegmentGroup.Root
       value={value}
       disabled={disabled}
@@ -160,17 +136,26 @@ export const SegmentGroup: React.FC<SegmentGroupProps> = ({
         }
       }}
     >
-      <div className={containerClassName}>{containerContent}</div>
+      <div className={containerClassName}>
+        <ArkSegmentGroup.Indicator className={indicatorStyle({ size })} />
+        {options.map((option) => (
+          <ArkSegmentGroup.Item
+            key={option.value}
+            value={option.value}
+            className={itemStyle({
+              isSelected: value === option.value,
+              size,
+              isDisabled: disabled,
+            })}
+          >
+            <ArkSegmentGroup.ItemText>{option.label}</ArkSegmentGroup.ItemText>
+            <ArkSegmentGroup.ItemControl />
+            <ArkSegmentGroup.ItemHiddenInput />
+          </ArkSegmentGroup.Item>
+        ))}
+      </div>
     </ArkSegmentGroup.Root>
   );
-
-  if (tooltip) {
-    return (
-      <Tooltip content={tooltip} display="block">
-        {segmentGroup}
-      </Tooltip>
-    );
-  }
-
-  return segmentGroup;
 };
+
+export const SegmentGroup = withTooltip(SegmentGroupBase, "block");
diff --git a/libs/@hashintel/petrinaut/src/components/switch.tsx b/libs/@hashintel/petrinaut/src/components/switch.tsx
index 9fef184fd7f..23ec285075c 100644
--- a/libs/@hashintel/petrinaut/src/components/switch.tsx
+++ b/libs/@hashintel/petrinaut/src/components/switch.tsx
@@ -1,7 +1,7 @@
 import { Switch as ArkSwitch } from "@ark-ui/react/switch";
 import { css } from "@hashintel/ds-helpers/css";
 
-import { Tooltip } from "./tooltip";
+import { withTooltip } from "./hoc/with-tooltip";
 
 const controlStyle = css({
   position: "relative",
@@ -41,37 +41,25 @@ interface SwitchProps {
   checked?: boolean;
   onCheckedChange?: (checked: boolean) => void;
   disabled?: boolean;
-  tooltip?: string;
 }
 
-export const Switch: React.FC<SwitchProps> = ({
+const SwitchBase: React.FC<SwitchProps> = ({
   checked,
   onCheckedChange,
   disabled = false,
-  tooltip,
-}) => {
-  const switchElement = (
-    <ArkSwitch.Root
-      checked={checked}
-      onCheckedChange={(details) => {
-        onCheckedChange?.(details.checked);
-      }}
-      disabled={disabled}
-    >
-      <ArkSwitch.Control className={controlStyle}>
-        <ArkSwitch.Thumb className={thumbStyle} />
-      </ArkSwitch.Control>
-      <ArkSwitch.HiddenInput />
-    </ArkSwitch.Root>
-  );
+}) => (
+  <ArkSwitch.Root
+    checked={checked}
+    onCheckedChange={(details) => {
+      onCheckedChange?.(details.checked);
+    }}
+    disabled={disabled}
+  >
+    <ArkSwitch.Control className={controlStyle}>
+      <ArkSwitch.Thumb className={thumbStyle} />
+    </ArkSwitch.Control>
+    <ArkSwitch.HiddenInput />
+  </ArkSwitch.Root>
+);
 
-  if (tooltip) {
-    return (
-      <Tooltip content={tooltip} display="inline">
-        {switchElement}
-      </Tooltip>
-    );
-  }
-
-  return switchElement;
-};
+export const Switch = withTooltip(SwitchBase, "inline");

From 1cc11b2035cddf2d86e41a256b7ac26b0beaf235 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 17:44:46 +0100
Subject: [PATCH 25/29] Add reusable form components and refactor properties
 panels

- Create Input, NumberInput, Slider, Button, IconButton, and Select components
- All components use withTooltip HoC for automatic tooltip support
- Replace native form elements with new components across:
  - parameter-properties.tsx
  - transition-properties.tsx
  - place-properties.tsx
  - sortable-arc-item.tsx
  - type-properties.tsx
  - differential-equation-properties.tsx
  - place-initial-state.tsx
- Remove redundant inline style definitions (inputStyle, deleteButtonStyle, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/button.tsx       |  82 +++++
 .../petrinaut/src/components/icon-button.tsx  | 116 +++++++
 .../petrinaut/src/components/input.tsx        |  72 +++++
 .../petrinaut/src/components/number-input.tsx |  63 ++++
 .../petrinaut/src/components/select.tsx       |  53 ++++
 .../petrinaut/src/components/slider.tsx       |  49 +++
 .../differential-equation-properties.tsx      | 119 +++----
 .../PropertiesPanel/parameter-properties.tsx  |  85 ++---
 .../PropertiesPanel/place-properties.tsx      | 300 +++++-------------
 .../PropertiesPanel/sortable-arc-item.tsx     |  59 +---
 .../PropertiesPanel/transition-properties.tsx | 121 ++-----
 .../PropertiesPanel/type-properties.tsx       | 201 ++++--------
 .../Editor/subviews/place-initial-state.tsx   |  37 +--
 13 files changed, 688 insertions(+), 669 deletions(-)
 create mode 100644 libs/@hashintel/petrinaut/src/components/button.tsx
 create mode 100644 libs/@hashintel/petrinaut/src/components/icon-button.tsx
 create mode 100644 libs/@hashintel/petrinaut/src/components/input.tsx
 create mode 100644 libs/@hashintel/petrinaut/src/components/number-input.tsx
 create mode 100644 libs/@hashintel/petrinaut/src/components/select.tsx
 create mode 100644 libs/@hashintel/petrinaut/src/components/slider.tsx

diff --git a/libs/@hashintel/petrinaut/src/components/button.tsx b/libs/@hashintel/petrinaut/src/components/button.tsx
new file mode 100644
index 00000000000..096ec2d3435
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/button.tsx
@@ -0,0 +1,82 @@
+import { cva } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const buttonStyle = cva({
+  base: {
+    display: "inline-flex",
+    alignItems: "center",
+    justifyContent: "center",
+    gap: "[6px]",
+    fontSize: "[12px]",
+    padding: "[4px 8px]",
+    border: "[1px solid rgba(0, 0, 0, 0.2)]",
+    borderRadius: "[4px]",
+    backgroundColor: "[white]",
+    color: "[#333]",
+    cursor: "pointer",
+    transition: "[all 0.15s ease]",
+    _hover: {
+      backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+    },
+    _active: {
+      backgroundColor: "[rgba(0, 0, 0, 0.1)]",
+    },
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        opacity: "[0.5]",
+        cursor: "not-allowed",
+        _hover: {
+          backgroundColor: "[white]",
+        },
+      },
+      false: {},
+    },
+    variant: {
+      default: {},
+      ghost: {
+        border: "none",
+        backgroundColor: "[transparent]",
+        _hover: {
+          backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+        },
+      },
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
+    variant: "default",
+  },
+});
+
+interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  /** Button variant */
+  variant?: "default" | "ghost";
+  /** Button content */
+  children: React.ReactNode;
+  /** Ref to the button element */
+  ref?: React.Ref<HTMLButtonElement>;
+}
+
+const ButtonBase: React.FC<ButtonProps> = ({
+  variant = "default",
+  disabled,
+  className,
+  children,
+  ref,
+  ...props
+}) => (
+  <button
+    ref={ref}
+    type="button"
+    disabled={disabled}
+    className={`${buttonStyle({ isDisabled: disabled, variant })}${className ? ` ${className}` : ""}`}
+    {...props}
+  >
+    {children}
+  </button>
+);
+
+export const Button = withTooltip(ButtonBase, "inline");
diff --git a/libs/@hashintel/petrinaut/src/components/icon-button.tsx b/libs/@hashintel/petrinaut/src/components/icon-button.tsx
new file mode 100644
index 00000000000..ac5511aaa33
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/icon-button.tsx
@@ -0,0 +1,116 @@
+import { cva } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const iconButtonStyle = cva({
+  base: {
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    padding: "0",
+    border: "none",
+    background: "[transparent]",
+    borderRadius: "md.4",
+    cursor: "pointer",
+    transition: "[all 0.15s ease]",
+  },
+  variants: {
+    size: {
+      sm: {
+        width: "[20px]",
+        height: "[20px]",
+        fontSize: "[14px]",
+      },
+      md: {
+        width: "[24px]",
+        height: "[24px]",
+        fontSize: "[16px]",
+      },
+      lg: {
+        width: "[32px]",
+        height: "[32px]",
+        fontSize: "[20px]",
+      },
+    },
+    variant: {
+      default: {
+        color: "gray.60",
+        _hover: {
+          color: "gray.80",
+          backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+        },
+      },
+      danger: {
+        color: "gray.60",
+        _hover: {
+          color: "red.60",
+          backgroundColor: "red.10",
+        },
+      },
+    },
+    isDisabled: {
+      true: {
+        opacity: "[0.5]",
+        cursor: "not-allowed",
+        _hover: {
+          color: "gray.60",
+          backgroundColor: "[transparent]",
+        },
+      },
+      false: {},
+    },
+  },
+  defaultVariants: {
+    size: "md",
+    variant: "default",
+    isDisabled: false,
+  },
+  compoundVariants: [
+    {
+      variant: "danger",
+      isDisabled: true,
+      css: {
+        _hover: {
+          color: "gray.60",
+          backgroundColor: "[transparent]",
+        },
+      },
+    },
+  ],
+});
+
+interface IconButtonProps
+  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  /** Size variant */
+  size?: "sm" | "md" | "lg";
+  /** Style variant */
+  variant?: "default" | "danger";
+  /** Icon content */
+  children: React.ReactNode;
+  /** Accessibility label (required for icon-only buttons) */
+  "aria-label": string;
+  /** Ref to the button element */
+  ref?: React.Ref<HTMLButtonElement>;
+}
+
+const IconButtonBase: React.FC<IconButtonProps> = ({
+  size = "md",
+  variant = "default",
+  disabled,
+  className,
+  children,
+  ref,
+  ...props
+}) => (
+  <button
+    ref={ref}
+    type="button"
+    disabled={disabled}
+    className={`${iconButtonStyle({ size, variant, isDisabled: disabled })}${className ? ` ${className}` : ""}`}
+    {...props}
+  >
+    {children}
+  </button>
+);
+
+export const IconButton = withTooltip(IconButtonBase, "inline");
diff --git a/libs/@hashintel/petrinaut/src/components/input.tsx b/libs/@hashintel/petrinaut/src/components/input.tsx
new file mode 100644
index 00000000000..ce83eb36bea
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/input.tsx
@@ -0,0 +1,72 @@
+import { cva } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const inputStyle = cva({
+  base: {
+    fontSize: "[14px]",
+    padding: "[6px 8px]",
+    border: "[1px solid rgba(0, 0, 0, 0.15)]",
+    borderRadius: "[4px]",
+    width: "[100%]",
+    boxSizing: "border-box",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        backgroundColor: "[rgba(0, 0, 0, 0.02)]",
+        cursor: "not-allowed",
+      },
+      false: {
+        backgroundColor: "[white]",
+        cursor: "text",
+      },
+    },
+    isMonospace: {
+      true: {
+        fontFamily: "[monospace]",
+      },
+      false: {},
+    },
+    hasError: {
+      true: {
+        borderColor: "[#ef4444]",
+      },
+      false: {},
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
+    isMonospace: false,
+    hasError: false,
+  },
+});
+
+interface InputProps
+  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+  /** Whether to use monospace font */
+  monospace?: boolean;
+  /** Whether the input has an error */
+  hasError?: boolean;
+  /** Ref to the input element */
+  ref?: React.Ref<HTMLInputElement>;
+}
+
+const InputBase: React.FC<InputProps> = ({
+  monospace = false,
+  hasError = false,
+  disabled,
+  className,
+  ref,
+  ...props
+}) => (
+  <input
+    ref={ref}
+    type="text"
+    disabled={disabled}
+    className={`${inputStyle({ isDisabled: disabled, isMonospace: monospace, hasError })}${className ? ` ${className}` : ""}`}
+    {...props}
+  />
+);
+
+export const Input = withTooltip(InputBase, "block");
diff --git a/libs/@hashintel/petrinaut/src/components/number-input.tsx b/libs/@hashintel/petrinaut/src/components/number-input.tsx
new file mode 100644
index 00000000000..9122cf382f5
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/number-input.tsx
@@ -0,0 +1,63 @@
+import { cva } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const numberInputStyle = cva({
+  base: {
+    fontSize: "[14px]",
+    padding: "[6px 8px]",
+    border: "[1px solid rgba(0, 0, 0, 0.15)]",
+    borderRadius: "[4px]",
+    width: "[100%]",
+    boxSizing: "border-box",
+    fontFamily: "[monospace]",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        backgroundColor: "[rgba(0, 0, 0, 0.02)]",
+        cursor: "not-allowed",
+      },
+      false: {
+        backgroundColor: "[white]",
+        cursor: "text",
+      },
+    },
+    hasError: {
+      true: {
+        borderColor: "[#ef4444]",
+      },
+      false: {},
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
+    hasError: false,
+  },
+});
+
+interface NumberInputProps
+  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+  /** Whether the input has an error */
+  hasError?: boolean;
+  /** Ref to the input element */
+  ref?: React.Ref<HTMLInputElement>;
+}
+
+const NumberInputBase: React.FC<NumberInputProps> = ({
+  hasError = false,
+  disabled,
+  className,
+  ref,
+  ...props
+}) => (
+  <input
+    ref={ref}
+    type="number"
+    disabled={disabled}
+    className={`${numberInputStyle({ isDisabled: disabled, hasError })}${className ? ` ${className}` : ""}`}
+    {...props}
+  />
+);
+
+export const NumberInput = withTooltip(NumberInputBase, "block");
diff --git a/libs/@hashintel/petrinaut/src/components/select.tsx b/libs/@hashintel/petrinaut/src/components/select.tsx
new file mode 100644
index 00000000000..f8694222683
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/select.tsx
@@ -0,0 +1,53 @@
+import { cva } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const selectStyle = cva({
+  base: {
+    fontSize: "[14px]",
+    padding: "[6px 8px]",
+    border: "[1px solid rgba(0, 0, 0, 0.1)]",
+    borderRadius: "[4px]",
+    width: "[100%]",
+    boxSizing: "border-box",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+        cursor: "not-allowed",
+      },
+      false: {
+        backgroundColor: "[white]",
+        cursor: "pointer",
+      },
+    },
+  },
+  defaultVariants: {
+    isDisabled: false,
+  },
+});
+
+interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
+  /** Ref to the select element */
+  ref?: React.Ref<HTMLSelectElement>;
+}
+
+const SelectBase: React.FC<SelectProps> = ({
+  disabled,
+  className,
+  children,
+  ref,
+  ...props
+}) => (
+  <select
+    ref={ref}
+    disabled={disabled}
+    className={`${selectStyle({ isDisabled: disabled })}${className ? ` ${className}` : ""}`}
+    {...props}
+  >
+    {children}
+  </select>
+);
+
+export const Select = withTooltip(SelectBase, "block");
diff --git a/libs/@hashintel/petrinaut/src/components/slider.tsx b/libs/@hashintel/petrinaut/src/components/slider.tsx
new file mode 100644
index 00000000000..e6b25bde862
--- /dev/null
+++ b/libs/@hashintel/petrinaut/src/components/slider.tsx
@@ -0,0 +1,49 @@
+import { css } from "@hashintel/ds-helpers/css";
+
+import { withTooltip } from "./hoc/with-tooltip";
+
+const sliderStyle = css({
+  height: "[4px]",
+  appearance: "none",
+  background: "gray.30",
+  borderRadius: "[2px]",
+  outline: "none",
+  cursor: "pointer",
+  "&:disabled": {
+    opacity: "[0.5]",
+    cursor: "not-allowed",
+  },
+  "&::-webkit-slider-thumb": {
+    appearance: "none",
+    width: "[12px]",
+    height: "[12px]",
+    borderRadius: "[50%]",
+    background: "blue.50",
+    cursor: "pointer",
+  },
+  "&::-moz-range-thumb": {
+    width: "[12px]",
+    height: "[12px]",
+    borderRadius: "[50%]",
+    background: "blue.50",
+    cursor: "pointer",
+    border: "none",
+  },
+});
+
+interface SliderProps
+  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+  /** Ref to the input element */
+  ref?: React.Ref<HTMLInputElement>;
+}
+
+const SliderBase: React.FC<SliderProps> = ({ className, ref, ...props }) => (
+  <input
+    ref={ref}
+    type="range"
+    className={`${sliderStyle}${className ? ` ${className}` : ""}`}
+    {...props}
+  />
+);
+
+export const Slider = withTooltip(SliderBase, "inline");
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
index 19fa17f4500..299ba8184ac 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
@@ -4,7 +4,9 @@ import { css, cva } from "@hashintel/ds-helpers/css";
 import { useState } from "react";
 import { TbDotsVertical, TbSparkles } from "react-icons/tb";
 
+import { Button } from "../../../../components/button";
 import { CodeEditor } from "../../../../components/code-editor";
+import { Input } from "../../../../components/input";
 import { Menu } from "../../../../components/menu";
 import { Tooltip } from "../../../../components/tooltip";
 import { UI_MESSAGES } from "../../../../constants/ui-messages";
@@ -38,51 +40,19 @@ const fieldLabelStyle = css({
   marginBottom: "[4px]",
 });
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
-});
-
 const typeDropdownButtonStyle = cva({
   base: {
     width: "[100%]",
     fontSize: "[14px]",
     padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
     display: "flex",
-    alignItems: "center",
     gap: "[8px]",
     textAlign: "left",
   },
   variants: {
-    isReadOnly: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "pointer",
-      },
+    isDisabled: {
+      true: {},
+      false: {},
     },
   },
 });
@@ -309,49 +279,41 @@ export const DifferentialEquationProperties: React.FC<
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={differentialEquation.name}
-            onChange={(event) => {
-              updateDifferentialEquation(
-                differentialEquation.id,
-                (existingEquation) => {
-                  existingEquation.name = event.target.value;
-                },
-              );
-            }}
-            disabled={isReadOnly}
-            className={inputStyle({ isReadOnly })}
-          />
-        </Tooltip>
+        <Input
+          value={differentialEquation.name}
+          onChange={(event) => {
+            updateDifferentialEquation(
+              differentialEquation.id,
+              (existingEquation) => {
+                existingEquation.name = event.target.value;
+              },
+            );
+          }}
+          disabled={isReadOnly}
+          tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       <div>
         <div className={fieldLabelStyle}>Associated Type</div>
         <div style={{ position: "relative" }}>
-          <Tooltip
-            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
-            display="inline"
+          <Button
+            onClick={() => setShowTypeDropdown(!showTypeDropdown)}
+            onBlur={() => setTimeout(() => setShowTypeDropdown(false), 200)}
+            disabled={isReadOnly}
+            className={typeDropdownButtonStyle({ isDisabled: isReadOnly })}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
           >
-            <button
-              type="button"
-              onClick={() => setShowTypeDropdown(!showTypeDropdown)}
-              onBlur={() => setTimeout(() => setShowTypeDropdown(false), 200)}
-              disabled={isReadOnly}
-              className={typeDropdownButtonStyle({ isReadOnly })}
-            >
-              {associatedType && (
-                <>
-                  <div
-                    className={colorDotStyle}
-                    style={{ backgroundColor: associatedType.displayColor }}
-                  />
-                  <span>{associatedType.name}</span>
-                </>
-              )}
-            </button>
-          </Tooltip>
+            {associatedType && (
+              <>
+                <div
+                  className={colorDotStyle}
+                  style={{ backgroundColor: associatedType.displayColor }}
+                />
+                <span>{associatedType.name}</span>
+              </>
+            )}
+          </Button>
           {showTypeDropdown && !isReadOnly && (
             <div className={dropdownMenuStyle}>
               {types.map((type) => (
@@ -429,20 +391,15 @@ export const DifferentialEquationProperties: React.FC<
               you want to continue?
             </div>
             <div className={confirmDialogButtonsStyle}>
-              <button
-                type="button"
-                onClick={cancelTypeChange}
-                className={cancelButtonStyle}
-              >
+              <Button onClick={cancelTypeChange} className={cancelButtonStyle}>
                 Cancel
-              </button>
-              <button
-                type="button"
+              </Button>
+              <Button
                 onClick={confirmTypeChange}
                 className={confirmButtonStyle}
               >
                 Change Type
-              </button>
+              </Button>
             </div>
           </div>
         </div>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
index c934c3bf1dc..a6f49c2b7d6 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx
@@ -1,6 +1,6 @@
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { css } from "@hashintel/ds-helpers/css";
 
-import { Tooltip } from "../../../../components/tooltip";
+import { Input } from "../../../../components/input";
 import { UI_MESSAGES } from "../../../../constants/ui-messages";
 import type { Parameter } from "../../../../core/types/sdcpn";
 import { useIsReadOnly } from "../../../../state/use-is-read-only";
@@ -23,38 +23,6 @@ const fieldLabelStyle = css({
   marginBottom: "[4px]",
 });
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.15)]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.02)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-    isMonospace: {
-      true: {
-        fontFamily: "[monospace]",
-      },
-      false: {},
-    },
-  },
-  defaultVariants: {
-    isDisabled: false,
-    isMonospace: false,
-  },
-});
-
 /**
  * Slugifies a string to a valid JavaScript identifier.
  * - Converts to lowercase
@@ -143,30 +111,25 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
       {/* Name field */}
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={parameter.name}
-            onChange={handleUpdateName}
-            disabled={isDisabled}
-            className={inputStyle({ isDisabled })}
-          />
-        </Tooltip>
+        <Input
+          value={parameter.name}
+          onChange={handleUpdateName}
+          disabled={isDisabled}
+          tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       {/* Variable Name field */}
       <div>
         <div className={fieldLabelStyle}>Variable Name</div>
-        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={parameter.variableName}
-            onChange={handleUpdateVariableName}
-            onBlur={handleBlurVariableName}
-            disabled={isDisabled}
-            className={inputStyle({ isDisabled, isMonospace: true })}
-          />
-        </Tooltip>
+        <Input
+          value={parameter.variableName}
+          onChange={handleUpdateVariableName}
+          onBlur={handleBlurVariableName}
+          disabled={isDisabled}
+          monospace
+          tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       {/* Type selector - hidden for now as internal code relies on "real" type */}
@@ -174,15 +137,13 @@ export const ParameterProperties: React.FC<ParameterPropertiesProps> = ({
       {/* Default Value field */}
       <div>
         <div className={fieldLabelStyle}>Default Value</div>
-        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={parameter.defaultValue}
-            onChange={handleUpdateDefaultValue}
-            disabled={isDisabled}
-            className={inputStyle({ isDisabled, isMonospace: true })}
-          />
-        </Tooltip>
+        <Input
+          value={parameter.defaultValue}
+          onChange={handleUpdateDefaultValue}
+          disabled={isDisabled}
+          monospace
+          tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
     </div>
   );
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
index a6d3e1e46b9..9fa8709d250 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx
@@ -1,5 +1,5 @@
 /* eslint-disable id-length */
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { css } from "@hashintel/ds-helpers/css";
 import MonacoEditor from "@monaco-editor/react";
 import { use, useEffect, useMemo, useRef, useState } from "react";
 import {
@@ -9,7 +9,11 @@ import {
   TbTrash,
 } from "react-icons/tb";
 
+import { Button } from "../../../../components/button";
+import { IconButton } from "../../../../components/icon-button";
+import { Input } from "../../../../components/input";
 import { Menu } from "../../../../components/menu";
+import { Select } from "../../../../components/select";
 import type { SubView } from "../../../../components/sub-view/types";
 import { FixedHeightSubViewsContainer } from "../../../../components/sub-view/vertical-sub-views-container";
 import { Switch } from "../../../../components/switch";
@@ -60,39 +64,6 @@ const headerTitleStyle = css({
   fontSize: "[16px]",
 });
 
-const deleteButtonStyle = cva({
-  base: {
-    display: "flex",
-    alignItems: "center",
-    justifyContent: "center",
-    width: "[24px]",
-    height: "[24px]",
-    padding: "0",
-    border: "none",
-    background: "[transparent]",
-    color: "gray.60",
-    borderRadius: "md.4",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        cursor: "not-allowed",
-        opacity: "[0.5]",
-      },
-      false: {
-        cursor: "pointer",
-        _hover: {
-          color: "red.60",
-          backgroundColor: "red.10",
-        },
-      },
-    },
-  },
-  defaultVariants: {
-    isDisabled: false,
-  },
-});
-
 const fieldLabelStyle = css({
   fontWeight: "medium",
   fontSize: "[12px]",
@@ -107,92 +78,16 @@ const fieldLabelWithTooltipStyle = css({
   alignItems: "center",
 });
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-    hasError: {
-      true: {
-        border: "[1px solid #ef4444]",
-      },
-      false: {
-        border: "[1px solid rgba(0, 0, 0, 0.1)]",
-      },
-    },
-  },
-  defaultVariants: {
-    isReadOnly: false,
-    hasError: false,
-  },
-});
-
 const errorMessageStyle = css({
   fontSize: "[12px]",
   color: "[#ef4444]",
   marginTop: "[4px]",
 });
 
-const selectStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "pointer",
-      },
-    },
-    hasMarginBottom: {
-      true: {
-        marginBottom: "[8px]",
-      },
-      false: {},
-    },
-  },
-});
-
 const jumpButtonContainerStyle = css({
   textAlign: "right",
 });
 
-const jumpButtonStyle = css({
-  fontSize: "[12px]",
-  padding: "[4px 8px]",
-  border: "[1px solid rgba(0, 0, 0, 0.2)]",
-  borderRadius: "[4px]",
-  backgroundColor: "[white]",
-  cursor: "pointer",
-  color: "[#333]",
-  display: "inline-flex",
-  alignItems: "center",
-  gap: "[6px]",
-});
-
 const jumpIconStyle = css({
   fontSize: "[14px]",
 });
@@ -398,56 +293,48 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
         <div>
           <div className={headerContainerStyle}>
             <div className={headerTitleStyle}>Place</div>
-            <Tooltip
-              content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
-              display="inline"
+            <IconButton
+              aria-label="Delete"
+              variant="danger"
+              onClick={() => {
+                if (
+                  // eslint-disable-next-line no-alert
+                  window.confirm(
+                    `Are you sure you want to delete "${place.name}"? All arcs connected to this place will also be removed.`,
+                  )
+                ) {
+                  removePlace(place.id);
+                }
+              }}
+              disabled={isReadOnly}
+              tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
             >
-              <button
-                type="button"
-                onClick={() => {
-                  if (
-                    // eslint-disable-next-line no-alert
-                    window.confirm(
-                      `Are you sure you want to delete "${place.name}"? All arcs connected to this place will also be removed.`,
-                    )
-                  ) {
-                    removePlace(place.id);
-                  }
-                }}
-                disabled={isReadOnly}
-                className={deleteButtonStyle({ isDisabled: isReadOnly })}
-              >
-                <TbTrash size={16} />
-              </button>
-            </Tooltip>
+              <TbTrash size={16} />
+            </IconButton>
           </div>
         </div>
 
         <div>
           <div className={fieldLabelStyle}>Name</div>
-          <Tooltip
-            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
-          >
-            <input
-              ref={nameInputRef}
-              type="text"
-              value={nameInputValue}
-              onChange={(event) => {
-                setNameInputValue(event.target.value);
-                // Clear error when user starts typing
-                if (nameError) {
-                  setNameError(null);
-                }
-              }}
-              onFocus={() => setIsNameInputFocused(true)}
-              onBlur={() => {
-                setIsNameInputFocused(false);
-                handleNameBlur();
-              }}
-              disabled={isReadOnly}
-              className={inputStyle({ isReadOnly, hasError: !!nameError })}
-            />
-          </Tooltip>
+          <Input
+            ref={nameInputRef}
+            value={nameInputValue}
+            onChange={(event) => {
+              setNameInputValue(event.target.value);
+              // Clear error when user starts typing
+              if (nameError) {
+                setNameError(null);
+              }
+            }}
+            onFocus={() => setIsNameInputFocused(true)}
+            onBlur={() => {
+              setIsNameInputFocused(false);
+              handleNameBlur();
+            }}
+            disabled={isReadOnly}
+            hasError={!!nameError}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          />
           {nameError && <div className={errorMessageStyle}>{nameError}</div>}
         </div>
 
@@ -462,49 +349,41 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
               } Tokens in places don't have to carry data, but they need one to enable dynamics (token data changing over time when in a place).`}
             />
           </div>
-          <Tooltip
-            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+          <Select
+            value={place.colorId ?? ""}
+            onChange={(event) => {
+              const value = event.target.value;
+              const newType = value === "" ? null : value;
+              updatePlace(place.id, (existingPlace) => {
+                existingPlace.colorId = newType;
+                // Disable dynamics if type is being set to null
+                if (newType === null && existingPlace.dynamicsEnabled) {
+                  existingPlace.dynamicsEnabled = false;
+                }
+              });
+            }}
+            disabled={isReadOnly}
+            style={place.colorId ? { marginBottom: "8px" } : undefined}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
           >
-            <select
-              value={place.colorId ?? ""}
-              onChange={(event) => {
-                const value = event.target.value;
-                const newType = value === "" ? null : value;
-                updatePlace(place.id, (existingPlace) => {
-                  existingPlace.colorId = newType;
-                  // Disable dynamics if type is being set to null
-                  if (newType === null && existingPlace.dynamicsEnabled) {
-                    existingPlace.dynamicsEnabled = false;
-                  }
-                });
-              }}
-              disabled={isReadOnly}
-              className={selectStyle({
-                isReadOnly,
-                hasMarginBottom: !!place.colorId,
-              })}
-            >
-              <option value="">None</option>
-              {types.map((type) => (
-                <option key={type.id} value={type.id}>
-                  {type.name}
-                </option>
-              ))}
-            </select>
-          </Tooltip>
+            <option value="">None</option>
+            {types.map((type) => (
+              <option key={type.id} value={type.id}>
+                {type.name}
+              </option>
+            ))}
+          </Select>
 
           {place.colorId && (
             <div className={jumpButtonContainerStyle}>
-              <button
-                type="button"
+              <Button
                 onClick={() => {
                   setSelectedResourceId(place.colorId);
                 }}
-                className={jumpButtonStyle}
               >
                 Jump to Type
                 <TbArrowRight className={jumpIconStyle} />
-              </button>
+              </Button>
             </div>
           )}
         </div>
@@ -550,42 +429,37 @@ export const PlaceProperties: React.FC<PlacePropertiesProps> = ({
           availableDiffEqs.length > 0 && (
             <div className={diffEqContainerStyle}>
               <div className={fieldLabelStyle}>Differential Equation</div>
-              <Tooltip
-                content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+              <Select
+                value={place.differentialEquationId ?? undefined}
+                onChange={(event) => {
+                  const value = event.target.value;
+
+                  updatePlace(place.id, (existingPlace) => {
+                    existingPlace.differentialEquationId = value || null;
+                  });
+                }}
+                disabled={isReadOnly}
+                style={{ marginBottom: "8px" }}
+                tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
               >
-                <select
-                  value={place.differentialEquationId ?? undefined}
-                  onChange={(event) => {
-                    const value = event.target.value;
-
-                    updatePlace(place.id, (existingPlace) => {
-                      existingPlace.differentialEquationId = value || null;
-                    });
-                  }}
-                  disabled={isReadOnly}
-                  className={selectStyle({ isReadOnly, hasMarginBottom: true })}
-                >
-                  <option value="">None</option>
-                  {availableDiffEqs.map((eq) => (
-                    <option key={eq.id} value={eq.id}>
-                      {eq.name}
-                    </option>
-                  ))}
-                </select>
-              </Tooltip>
+                <option value="">None</option>
+                {availableDiffEqs.map((eq) => (
+                  <option key={eq.id} value={eq.id}>
+                    {eq.name}
+                  </option>
+                ))}
+              </Select>
 
               {place.differentialEquationId && (
                 <div className={jumpButtonContainerStyle}>
-                  <button
-                    type="button"
+                  <Button
                     onClick={() => {
                       setSelectedResourceId(place.differentialEquationId);
                     }}
-                    className={jumpButtonStyle}
                   >
                     Jump to Differential Equation
                     <TbArrowRight className={jumpIconStyle} />
-                  </button>
+                  </Button>
                 </div>
               )}
             </div>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
index 55c947d3b4b..3517d97ea4c 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
@@ -4,6 +4,8 @@ import { css, cva } from "@hashintel/ds-helpers/css";
 import { MdDragIndicator } from "react-icons/md";
 import { TbTrash } from "react-icons/tb";
 
+import { IconButton } from "../../../../components/icon-button";
+import { NumberInput } from "../../../../components/number-input";
 import { FEATURE_FLAGS } from "../../../../feature-flags";
 
 const containerStyle = css({
@@ -60,46 +62,10 @@ const weightLabelStyle = css({
   fontWeight: "medium",
 });
 
-const weightInputStyle = cva({
-  base: {
-    width: "[60px]",
-    fontSize: "[14px]",
-    padding: "[4px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
-});
-
-const deleteButtonStyle = css({
-  display: "flex",
-  alignItems: "center",
-  justifyContent: "center",
-  width: "[24px]",
-  height: "[24px]",
-  padding: "0",
-  border: "none",
-  background: "[transparent]",
-  cursor: "pointer",
-  color: "gray.60",
-  flexShrink: 0,
-  borderRadius: "md.4",
-  _hover: {
-    color: "red.60",
-    backgroundColor: "red.10",
-  },
+const weightInputStyle = css({
+  width: "[60px]",
+  fontSize: "[14px]",
+  padding: "[4px 8px]",
 });
 
 /**
@@ -151,10 +117,9 @@ export const SortableArcItem: React.FC<SortableArcItemProps> = ({
       <div className={placeNameStyle}>{placeName}</div>
       <div className={weightContainerStyle}>
         <span className={weightLabelStyle}>weight</span>
-        <input
-          type="number"
-          min="1"
-          step="1"
+        <NumberInput
+          min={1}
+          step={1}
           value={weight}
           disabled={disabled}
           onChange={(event) => {
@@ -163,13 +128,13 @@ export const SortableArcItem: React.FC<SortableArcItemProps> = ({
               onWeightChange(newWeight);
             }
           }}
-          className={weightInputStyle({ isDisabled: disabled })}
+          className={weightInputStyle}
         />
       </div>
       {onDelete && !disabled && (
-        <button type="button" onClick={onDelete} className={deleteButtonStyle}>
+        <IconButton aria-label="Delete" variant="danger" onClick={onDelete}>
           <TbTrash size={16} />
-        </button>
+        </IconButton>
       )}
     </div>
   );
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index b686fb6eee3..9ab14003177 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -15,11 +15,13 @@ import {
   sortableKeyboardCoordinates,
   verticalListSortingStrategy,
 } from "@dnd-kit/sortable";
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { css } from "@hashintel/ds-helpers/css";
 import { use } from "react";
 import { TbDotsVertical, TbSparkles, TbTrash } from "react-icons/tb";
 
 import { CodeEditor } from "../../../../components/code-editor";
+import { IconButton } from "../../../../components/icon-button";
+import { Input } from "../../../../components/input";
 import { Menu } from "../../../../components/menu";
 import { SegmentGroup } from "../../../../components/segment-group";
 import { InfoIconTooltip, Tooltip } from "../../../../components/tooltip";
@@ -52,68 +54,12 @@ const headerTitleStyle = css({
   fontSize: "[16px]",
 });
 
-const deleteButtonStyle = cva({
-  base: {
-    display: "flex",
-    alignItems: "center",
-    justifyContent: "center",
-    width: "[24px]",
-    height: "[24px]",
-    padding: "0",
-    border: "none",
-    background: "[transparent]",
-    color: "gray.60",
-    borderRadius: "md.4",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        cursor: "not-allowed",
-        opacity: "[0.5]",
-      },
-      false: {
-        cursor: "pointer",
-        _hover: {
-          color: "red.60",
-          backgroundColor: "red.10",
-        },
-      },
-    },
-  },
-  defaultVariants: {
-    isDisabled: false,
-  },
-});
-
 const fieldLabelStyle = css({
   fontWeight: "medium",
   fontSize: "[12px]",
   marginBottom: "[4px]",
 });
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isReadOnly: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
-});
-
 const sectionContainerStyle = css({
   marginTop: "[20px]",
 });
@@ -319,46 +265,39 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
       <div>
         <div className={headerContainerStyle}>
           <div className={headerTitleStyle}>Transition</div>
-          <Tooltip
-            content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
-            display="inline"
+          <IconButton
+            aria-label="Delete"
+            variant="danger"
+            onClick={() => {
+              if (
+                // eslint-disable-next-line no-alert
+                window.confirm(
+                  `Are you sure you want to delete "${transition.name}"? All arcs connected to this transition will also be removed.`,
+                )
+              ) {
+                removeTransition(transition.id);
+              }
+            }}
+            disabled={isReadOnly}
+            tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"}
           >
-            <button
-              type="button"
-              onClick={() => {
-                if (
-                  // eslint-disable-next-line no-alert
-                  window.confirm(
-                    `Are you sure you want to delete "${transition.name}"? All arcs connected to this transition will also be removed.`,
-                  )
-                ) {
-                  removeTransition(transition.id);
-                }
-              }}
-              disabled={isReadOnly}
-              className={deleteButtonStyle({ isDisabled: isReadOnly })}
-            >
-              <TbTrash size={16} />
-            </button>
-          </Tooltip>
+            <TbTrash size={16} />
+          </IconButton>
         </div>
       </div>
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <Tooltip content={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={transition.name}
-            onChange={(event) => {
-              updateTransition(transition.id, (existingTransition) => {
-                existingTransition.name = event.target.value;
-              });
-            }}
-            disabled={isReadOnly}
-            className={inputStyle({ isReadOnly })}
-          />
-        </Tooltip>
+        <Input
+          value={transition.name}
+          onChange={(event) => {
+            updateTransition(transition.id, (existingTransition) => {
+              existingTransition.name = event.target.value;
+            });
+          }}
+          disabled={isReadOnly}
+          tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       <div className={sectionContainerStyle}>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
index b05182a5d4e..dc1fa41e6eb 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
@@ -2,6 +2,8 @@ import { css, cva } from "@hashintel/ds-helpers/css";
 import { useState } from "react";
 import { v4 as uuidv4 } from "uuid";
 
+import { Button } from "../../../../components/button";
+import { Input } from "../../../../components/input";
 import { Tooltip } from "../../../../components/tooltip";
 import { UI_MESSAGES } from "../../../../constants/ui-messages";
 import type { Color } from "../../../../core/types/sdcpn";
@@ -26,29 +28,6 @@ const fieldLabelStyle = css({
   marginBottom: "[4px]",
 });
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
-});
-
 const dimensionsHeaderStyle = css({
   display: "flex",
   alignItems: "center",
@@ -68,29 +47,11 @@ const dimensionsHintStyle = css({
   fontWeight: "normal",
 });
 
-const addDimensionButtonStyle = cva({
-  base: {
-    fontSize: "[16px]",
-    padding: "[2px 8px]",
-    borderRadius: "[4px]",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-    fontWeight: "semibold",
-    cursor: "pointer",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        color: "[#999]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[rgba(59, 130, 246, 0.1)]",
-        color: "[#3b82f6]",
-        cursor: "pointer",
-      },
-    },
-  },
+const addDimensionButtonStyle = css({
+  fontSize: "[16px]",
+  padding: "[2px 8px]",
+  backgroundColor: "[rgba(59, 130, 246, 0.1)]",
+  color: "[#3b82f6]",
 });
 
 const emptyDimensionsStyle = css({
@@ -180,26 +141,10 @@ const indexChipStyle = css({
   flexShrink: 0,
 });
 
-const dimensionNameInputStyle = cva({
-  base: {
-    fontSize: "[13px]",
-    padding: "[5px 8px]",
-    border: "[1px solid rgba(0, 0, 0, 0.15)]",
-    borderRadius: "[3px]",
-    flex: "1",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.02)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
+const dimensionNameInputStyle = css({
+  fontSize: "[13px]",
+  padding: "[5px 8px]",
+  flex: "1",
 });
 
 const deleteDimensionButtonStyle = css({
@@ -210,22 +155,9 @@ const deleteDimensionButtonStyle = css({
   border: "[1px solid rgba(239, 68, 68, 0.2)]",
   backgroundColor: "[rgba(239, 68, 68, 0.08)]",
   color: "[#ef4444]",
-  cursor: "pointer",
   fontWeight: "semibold",
   lineHeight: "[1]",
-  transition: "[all 0.15s ease]",
   flexShrink: 0,
-  display: "flex",
-  alignItems: "center",
-  justifyContent: "center",
-  _hover: {
-    backgroundColor: "[rgba(239, 68, 68, 0.15)]",
-  },
-  _disabled: {
-    backgroundColor: "[rgba(0, 0, 0, 0.02)]",
-    color: "[#ccc]",
-    cursor: "not-allowed",
-  },
 });
 
 // --- Helpers ---
@@ -404,19 +336,16 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
 
       <div>
         <div className={fieldLabelStyle}>Name</div>
-        <Tooltip content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}>
-          <input
-            type="text"
-            value={type.name}
-            onChange={(event) => {
-              updateType(type.id, (existingType) => {
-                existingType.name = event.target.value;
-              });
-            }}
-            disabled={isDisabled}
-            className={inputStyle({ isDisabled })}
-          />
-        </Tooltip>
+        <Input
+          value={type.name}
+          onChange={(event) => {
+            updateType(type.id, (existingType) => {
+              existingType.name = event.target.value;
+            });
+          }}
+          disabled={isDisabled}
+          tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+        />
       </div>
 
       <div>
@@ -441,20 +370,15 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
             Dimensions
             <span className={dimensionsHintStyle}>(order matters)</span>
           </div>
-          <Tooltip
-            content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
-            display="inline"
+          <Button
+            onClick={handleAddElement}
+            disabled={isDisabled}
+            className={addDimensionButtonStyle}
+            aria-label="Add dimension"
+            tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
           >
-            <button
-              type="button"
-              onClick={handleAddElement}
-              disabled={isDisabled}
-              className={addDimensionButtonStyle({ isDisabled })}
-              aria-label="Add dimension"
-            >
-              +
-            </button>
-          </Tooltip>
+            +
+          </Button>
         </div>
 
         {type.elements.length === 0 ? (
@@ -493,47 +417,38 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                 <div className={indexChipStyle}>{index}</div>
 
                 {/* Name input */}
-                <Tooltip
-                  content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
-                >
-                  <input
-                    type="text"
-                    value={element.name}
-                    onChange={(event) => {
-                      handleUpdateElementName(
-                        element.elementId,
-                        event.target.value,
-                      );
-                    }}
-                    onBlur={(event) => {
-                      handleBlurElementName(
-                        element.elementId,
-                        event.target.value,
-                      );
-                    }}
-                    disabled={isDisabled}
-                    placeholder="dimension_name"
-                    className={dimensionNameInputStyle({ isDisabled })}
-                  />
-                </Tooltip>
+                <Input
+                  value={element.name}
+                  onChange={(event) => {
+                    handleUpdateElementName(
+                      element.elementId,
+                      event.target.value,
+                    );
+                  }}
+                  onBlur={(event) => {
+                    handleBlurElementName(
+                      element.elementId,
+                      event.target.value,
+                    );
+                  }}
+                  disabled={isDisabled}
+                  placeholder="dimension_name"
+                  className={dimensionNameInputStyle}
+                  tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
+                />
 
                 {/* Delete button */}
-                <Tooltip
-                  content={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
-                  display="inline"
+                <Button
+                  onClick={() => {
+                    handleDeleteElement(element.elementId, element.name);
+                  }}
+                  disabled={isDisabled || type.elements.length === 1}
+                  className={deleteDimensionButtonStyle}
+                  aria-label={`Delete dimension ${element.name}`}
+                  tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
                 >
-                  <button
-                    type="button"
-                    onClick={() => {
-                      handleDeleteElement(element.elementId, element.name);
-                    }}
-                    disabled={isDisabled || type.elements.length === 1}
-                    className={deleteDimensionButtonStyle}
-                    aria-label={`Delete dimension ${element.name}`}
-                  >
-                    ×
-                  </button>
-                </Tooltip>
+                  ×
+                </Button>
               </div>
             ))}
           </div>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
index 60f71c98329..1f823fd3cdc 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
@@ -1,38 +1,13 @@
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { css } from "@hashintel/ds-helpers/css";
 import { use } from "react";
 import { TbTrash } from "react-icons/tb";
 
+import { NumberInput } from "../../../components/number-input";
 import type { SubView } from "../../../components/sub-view/types";
 import { SimulationContext } from "../../../simulation/context";
 import { InitialStateEditor } from "../panels/PropertiesPanel/initial-state-editor";
 import { usePlacePropertiesContext } from "../panels/PropertiesPanel/place-properties-context";
 
-const inputStyle = cva({
-  base: {
-    fontSize: "[14px]",
-    padding: "[6px 8px]",
-    borderRadius: "[4px]",
-    width: "[100%]",
-    boxSizing: "border-box",
-    border: "[1px solid rgba(0, 0, 0, 0.1)]",
-  },
-  variants: {
-    isDisabled: {
-      true: {
-        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
-        cursor: "not-allowed",
-      },
-      false: {
-        backgroundColor: "[white]",
-        cursor: "text",
-      },
-    },
-  },
-  defaultVariants: {
-    isDisabled: false,
-  },
-});
-
 const fieldLabelStyle = css({
   fontWeight: "medium",
   fontSize: "[12px]",
@@ -127,10 +102,9 @@ const PlaceInitialStateContent: React.FC = () => {
     return (
       <div className={simpleStateContainerStyle}>
         <div className={fieldLabelStyle}>Token count</div>
-        <input
-          type="number"
-          min="0"
-          step="1"
+        <NumberInput
+          min={0}
+          step={1}
           value={currentTokenCount}
           onChange={(event) => {
             const count = Math.max(
@@ -143,7 +117,6 @@ const PlaceInitialStateContent: React.FC = () => {
             });
           }}
           disabled={hasSimulationFrames}
-          className={inputStyle({ isDisabled: hasSimulationFrames })}
         />
       </div>
     );

From 9b4418692875d03add9c7ef12552237d32379d6e Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 18:20:56 +0100
Subject: [PATCH 26/29] Replace interface with type in form component props

Use type aliases instead of interfaces for component props to follow
project conventions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 libs/@hashintel/petrinaut/src/components/button.tsx       | 4 ++--
 .../petrinaut/src/components/hoc/with-tooltip.tsx         | 4 ++--
 libs/@hashintel/petrinaut/src/components/icon-button.tsx  | 5 ++---
 libs/@hashintel/petrinaut/src/components/input.tsx        | 5 ++---
 libs/@hashintel/petrinaut/src/components/number-input.tsx | 8 +++++---
 libs/@hashintel/petrinaut/src/components/select.tsx       | 4 ++--
 libs/@hashintel/petrinaut/src/components/slider.tsx       | 5 ++---
 7 files changed, 17 insertions(+), 18 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/button.tsx b/libs/@hashintel/petrinaut/src/components/button.tsx
index 096ec2d3435..6f554e9ef94 100644
--- a/libs/@hashintel/petrinaut/src/components/button.tsx
+++ b/libs/@hashintel/petrinaut/src/components/button.tsx
@@ -51,14 +51,14 @@ const buttonStyle = cva({
   },
 });
 
-interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
   /** Button variant */
   variant?: "default" | "ghost";
   /** Button content */
   children: React.ReactNode;
   /** Ref to the button element */
   ref?: React.Ref<HTMLButtonElement>;
-}
+};
 
 const ButtonBase: React.FC<ButtonProps> = ({
   variant = "default",
diff --git a/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx
index 7ef7908a513..dac664db87d 100644
--- a/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx
@@ -2,7 +2,7 @@ import type { ComponentType } from "react";
 
 import { Tooltip } from "../tooltip";
 
-interface WithTooltipProps {
+type WithTooltipProps = {
   /** Tooltip to show when hovering (useful for explaining disabled state). */
   tooltip?: string;
   /**
@@ -11,7 +11,7 @@ interface WithTooltipProps {
    * - "inline": For inline elements like buttons in flex containers
    */
   tooltipDisplay?: "block" | "inline";
-}
+};
 
 /**
  * Higher-Order Component that adds tooltip support to any component.
diff --git a/libs/@hashintel/petrinaut/src/components/icon-button.tsx b/libs/@hashintel/petrinaut/src/components/icon-button.tsx
index ac5511aaa33..048a519f714 100644
--- a/libs/@hashintel/petrinaut/src/components/icon-button.tsx
+++ b/libs/@hashintel/petrinaut/src/components/icon-button.tsx
@@ -79,8 +79,7 @@ const iconButtonStyle = cva({
   ],
 });
 
-interface IconButtonProps
-  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
   /** Size variant */
   size?: "sm" | "md" | "lg";
   /** Style variant */
@@ -91,7 +90,7 @@ interface IconButtonProps
   "aria-label": string;
   /** Ref to the button element */
   ref?: React.Ref<HTMLButtonElement>;
-}
+};
 
 const IconButtonBase: React.FC<IconButtonProps> = ({
   size = "md",
diff --git a/libs/@hashintel/petrinaut/src/components/input.tsx b/libs/@hashintel/petrinaut/src/components/input.tsx
index ce83eb36bea..539b726f8b0 100644
--- a/libs/@hashintel/petrinaut/src/components/input.tsx
+++ b/libs/@hashintel/petrinaut/src/components/input.tsx
@@ -42,15 +42,14 @@ const inputStyle = cva({
   },
 });
 
-interface InputProps
-  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> & {
   /** Whether to use monospace font */
   monospace?: boolean;
   /** Whether the input has an error */
   hasError?: boolean;
   /** Ref to the input element */
   ref?: React.Ref<HTMLInputElement>;
-}
+};
 
 const InputBase: React.FC<InputProps> = ({
   monospace = false,
diff --git a/libs/@hashintel/petrinaut/src/components/number-input.tsx b/libs/@hashintel/petrinaut/src/components/number-input.tsx
index 9122cf382f5..c364c8d8fbb 100644
--- a/libs/@hashintel/petrinaut/src/components/number-input.tsx
+++ b/libs/@hashintel/petrinaut/src/components/number-input.tsx
@@ -36,13 +36,15 @@ const numberInputStyle = cva({
   },
 });
 
-interface NumberInputProps
-  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+type NumberInputProps = Omit<
+  React.InputHTMLAttributes<HTMLInputElement>,
+  "type"
+> & {
   /** Whether the input has an error */
   hasError?: boolean;
   /** Ref to the input element */
   ref?: React.Ref<HTMLInputElement>;
-}
+};
 
 const NumberInputBase: React.FC<NumberInputProps> = ({
   hasError = false,
diff --git a/libs/@hashintel/petrinaut/src/components/select.tsx b/libs/@hashintel/petrinaut/src/components/select.tsx
index f8694222683..09594a7e8e4 100644
--- a/libs/@hashintel/petrinaut/src/components/select.tsx
+++ b/libs/@hashintel/petrinaut/src/components/select.tsx
@@ -28,10 +28,10 @@ const selectStyle = cva({
   },
 });
 
-interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
+type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
   /** Ref to the select element */
   ref?: React.Ref<HTMLSelectElement>;
-}
+};
 
 const SelectBase: React.FC<SelectProps> = ({
   disabled,
diff --git a/libs/@hashintel/petrinaut/src/components/slider.tsx b/libs/@hashintel/petrinaut/src/components/slider.tsx
index e6b25bde862..384519703c1 100644
--- a/libs/@hashintel/petrinaut/src/components/slider.tsx
+++ b/libs/@hashintel/petrinaut/src/components/slider.tsx
@@ -31,11 +31,10 @@ const sliderStyle = css({
   },
 });
 
-interface SliderProps
-  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
+type SliderProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> & {
   /** Ref to the input element */
   ref?: React.Ref<HTMLInputElement>;
-}
+};
 
 const SliderBase: React.FC<SliderProps> = ({ className, ref, ...props }) => (
   <input

From 68ef34f78e63aba81142fe8cf95d43a96cf2404b Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Mon, 26 Jan 2026 19:09:26 +0100
Subject: [PATCH 27/29] Fix CodeEditor tooltip and listener issues from AI
 review

- Fix memory leak by adding cleanup useEffect for timeout
- Fix edit listener not registering when readOnly changes dynamically
  by storing editor ref and using useEffect to manage listener lifecycle
- Fix tooltip not showing on hover in read-only mode by combining
  isHovering and showReadOnlyTooltip states
- Rename 'editor' variable to 'editorElement' to avoid shadowing
- Add display="inline" to AI menu tooltip in transition-properties

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/code-editor.tsx  | 82 ++++++++++++++++---
 .../PropertiesPanel/transition-properties.tsx |  5 +-
 2 files changed, 74 insertions(+), 13 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
index 7df2468de6a..64a44c43839 100644
--- a/libs/@hashintel/petrinaut/src/components/code-editor.tsx
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -2,8 +2,8 @@ import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
 import type { EditorProps, Monaco } from "@monaco-editor/react";
 import MonacoEditor from "@monaco-editor/react";
-import type { editor } from "monaco-editor";
-import { useCallback, useRef, useState } from "react";
+import type { editor, IDisposable } from "monaco-editor";
+import { useCallback, useEffect, useRef, useState } from "react";
 
 import { Tooltip } from "./tooltip";
 
@@ -54,12 +54,32 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
 }) => {
   const isReadOnly = options?.readOnly === true;
   const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false);
+  const [isHovering, setIsHovering] = useState(false);
   const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
+  const editAttemptListenerRef = useRef<IDisposable | null>(null);
 
-  const handleMount = useCallback(
-    (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
-      if (isReadOnly && tooltip) {
-        editorInstance.onDidAttemptReadOnlyEdit(() => {
+  // Cleanup timeout on unmount
+  useEffect(() => {
+    return () => {
+      if (hideTimeoutRef.current) {
+        clearTimeout(hideTimeoutRef.current);
+      }
+    };
+  }, []);
+
+  // Register/unregister edit attempt listener when isReadOnly changes
+  useEffect(() => {
+    // Dispose previous listener if exists
+    if (editAttemptListenerRef.current) {
+      editAttemptListenerRef.current.dispose();
+      editAttemptListenerRef.current = null;
+    }
+
+    // Register new listener if in read-only mode with tooltip
+    if (isReadOnly && tooltip && editorRef.current) {
+      editAttemptListenerRef.current =
+        editorRef.current.onDidAttemptReadOnlyEdit(() => {
           // Clear any existing timeout
           if (hideTimeoutRef.current) {
             clearTimeout(hideTimeoutRef.current);
@@ -73,6 +93,37 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
             setShowReadOnlyTooltip(false);
           }, 2000);
         });
+    }
+
+    return () => {
+      if (editAttemptListenerRef.current) {
+        editAttemptListenerRef.current.dispose();
+        editAttemptListenerRef.current = null;
+      }
+    };
+  }, [isReadOnly, tooltip]);
+
+  const handleMount = useCallback(
+    (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
+      editorRef.current = editorInstance;
+
+      // Register listener if already in read-only mode
+      if (isReadOnly && tooltip) {
+        editAttemptListenerRef.current =
+          editorInstance.onDidAttemptReadOnlyEdit(() => {
+            // Clear any existing timeout
+            if (hideTimeoutRef.current) {
+              clearTimeout(hideTimeoutRef.current);
+            }
+
+            // Show tooltip
+            setShowReadOnlyTooltip(true);
+
+            // Hide after 2 seconds
+            hideTimeoutRef.current = setTimeout(() => {
+              setShowReadOnlyTooltip(false);
+            }, 2000);
+          });
       }
 
       // Call the original onMount if provided
@@ -95,7 +146,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
     ...options,
   };
 
-  const editor = (
+  const editorElement = (
     <div className={containerStyle({ isReadOnly })} style={{ height }}>
       <MonacoEditor
         theme="vs-light"
@@ -107,17 +158,24 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
     </div>
   );
 
-  // In read-only mode with tooltip, use controlled tooltip that shows on edit attempts
+  // In read-only mode with tooltip, show on hover OR on edit attempt
   if (isReadOnly && tooltip) {
+    const isTooltipOpen = isHovering || showReadOnlyTooltip;
+
     return (
       <ArkTooltip.Root
-        open={showReadOnlyTooltip}
+        open={isTooltipOpen}
         openDelay={200}
         closeDelay={0}
         positioning={{ placement: "top" }}
       >
         <ArkTooltip.Trigger asChild>
-          <div>{editor}</div>
+          <div
+            onMouseEnter={() => setIsHovering(true)}
+            onMouseLeave={() => setIsHovering(false)}
+          >
+            {editorElement}
+          </div>
         </ArkTooltip.Trigger>
         <ArkTooltip.Positioner>
           <ArkTooltip.Content className={tooltipContentStyle}>
@@ -130,8 +188,8 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
 
   // Regular tooltip for non-read-only mode (if tooltip is provided)
   if (tooltip) {
-    return <Tooltip content={tooltip}>{editor}</Tooltip>;
+    return <Tooltip content={tooltip}>{editorElement}</Tooltip>;
   }
 
-  return editor;
+  return editorElement;
 };
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index 9ab14003177..e407f7d4fd8 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -557,7 +557,10 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
                   {
                     id: "generate-ai",
                     label: (
-                      <Tooltip content={UI_MESSAGES.AI_FEATURE_COMING_SOON}>
+                      <Tooltip
+                        content={UI_MESSAGES.AI_FEATURE_COMING_SOON}
+                        display="inline"
+                      >
                         <div className={aiMenuItemStyle}>
                           <TbSparkles className={aiIconStyle} />
                           Generate with AI

From 1f85b903957c26a7cbbb1db16d33c44b9c983f6c Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Tue, 27 Jan 2026 02:03:33 +0100
Subject: [PATCH 28/29] Fix duplicate listener registration and add arc
 tooltips

- Fix CodeEditor duplicate listener by using isEditorMounted state
  to trigger useEffect after Monaco mounts, removing registration
  from handleMount callback
- Add tooltip prop to SortableArcItem for read-only mode feedback
- Pass read-only tooltip to Input/Output Arc weight inputs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/code-editor.tsx  | 30 ++++---------------
 .../PropertiesPanel/sortable-arc-item.tsx     |  4 +++
 .../PropertiesPanel/transition-properties.tsx |  6 ++++
 3 files changed, 16 insertions(+), 24 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
index 64a44c43839..18847c32605 100644
--- a/libs/@hashintel/petrinaut/src/components/code-editor.tsx
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -55,6 +55,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
   const isReadOnly = options?.readOnly === true;
   const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false);
   const [isHovering, setIsHovering] = useState(false);
+  const [isEditorMounted, setIsEditorMounted] = useState(false);
   const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
   const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
   const editAttemptListenerRef = useRef<IDisposable | null>(null);
@@ -68,7 +69,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
     };
   }, []);
 
-  // Register/unregister edit attempt listener when isReadOnly changes
+  // Register/unregister edit attempt listener when isReadOnly changes or editor mounts
   useEffect(() => {
     // Dispose previous listener if exists
     if (editAttemptListenerRef.current) {
@@ -76,7 +77,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
       editAttemptListenerRef.current = null;
     }
 
-    // Register new listener if in read-only mode with tooltip
+    // Register new listener if in read-only mode with tooltip and editor is mounted
     if (isReadOnly && tooltip && editorRef.current) {
       editAttemptListenerRef.current =
         editorRef.current.onDidAttemptReadOnlyEdit(() => {
@@ -101,35 +102,16 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
         editAttemptListenerRef.current = null;
       }
     };
-  }, [isReadOnly, tooltip]);
+  }, [isReadOnly, tooltip, isEditorMounted]);
 
   const handleMount = useCallback(
     (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
       editorRef.current = editorInstance;
-
-      // Register listener if already in read-only mode
-      if (isReadOnly && tooltip) {
-        editAttemptListenerRef.current =
-          editorInstance.onDidAttemptReadOnlyEdit(() => {
-            // Clear any existing timeout
-            if (hideTimeoutRef.current) {
-              clearTimeout(hideTimeoutRef.current);
-            }
-
-            // Show tooltip
-            setShowReadOnlyTooltip(true);
-
-            // Hide after 2 seconds
-            hideTimeoutRef.current = setTimeout(() => {
-              setShowReadOnlyTooltip(false);
-            }, 2000);
-          });
-      }
-
+      setIsEditorMounted(true);
       // Call the original onMount if provided
       onMount?.(editorInstance, monaco);
     },
-    [isReadOnly, tooltip, onMount],
+    [onMount],
   );
 
   const editorOptions: EditorProps["options"] = {
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
index 3517d97ea4c..d68cb37ac3c 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx
@@ -76,6 +76,8 @@ interface SortableArcItemProps {
   placeName: string;
   weight: number;
   disabled?: boolean;
+  /** Tooltip to show when disabled (e.g., for read-only mode) */
+  tooltip?: string;
   onWeightChange: (weight: number) => void;
   onDelete?: () => void;
 }
@@ -85,6 +87,7 @@ export const SortableArcItem: React.FC<SortableArcItemProps> = ({
   placeName,
   weight,
   disabled = false,
+  tooltip,
   onWeightChange,
   onDelete,
 }) => {
@@ -122,6 +125,7 @@ export const SortableArcItem: React.FC<SortableArcItemProps> = ({
           step={1}
           value={weight}
           disabled={disabled}
+          tooltip={tooltip}
           onChange={(event) => {
             const newWeight = Number.parseInt(event.target.value, 10);
             if (!Number.isNaN(newWeight) && newWeight >= 1) {
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
index e407f7d4fd8..22201b754ee 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx
@@ -328,6 +328,9 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
                       placeName={place?.name ?? arc.placeId}
                       weight={arc.weight}
                       disabled={isReadOnly}
+                      tooltip={
+                        isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined
+                      }
                       onWeightChange={(weight) => {
                         onArcWeightUpdate(
                           transition.id,
@@ -374,6 +377,9 @@ export const TransitionProperties: React.FC<TransitionPropertiesProps> = ({
                       placeName={place?.name ?? arc.placeId}
                       weight={arc.weight}
                       disabled={isReadOnly}
+                      tooltip={
+                        isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined
+                      }
                       onWeightChange={(weight) => {
                         onArcWeightUpdate(
                           transition.id,

From 99fdc4de217bb894bfb340d08c4673a8ac9300f6 Mon Sep 17 00:00:00 2001
From: Chris Feijoo <chris@kube.io>
Date: Wed, 28 Jan 2026 18:59:25 +0100
Subject: [PATCH 29/29] Improve type dropdown UX and simplify CodeEditor
 tooltip

- Add placeholder text and empty state for type dropdown in differential equation properties
- Simplify CodeEditor tooltip by using standard Tooltip component with className support
- Add className prop to Tooltip component using cx helper
- Update button styles with disabled variant styling for type properties
- Add read-only tooltip to place initial state number input

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---
 .../petrinaut/src/components/button.tsx       |   2 +-
 .../petrinaut/src/components/code-editor.tsx  | 103 ++----------------
 .../petrinaut/src/components/tooltip.tsx      |   9 +-
 .../differential-equation-properties.tsx      |  84 ++++++++------
 .../PropertiesPanel/type-properties.tsx       |  63 ++++++++---
 .../Editor/subviews/place-initial-state.tsx   |   2 +
 6 files changed, 117 insertions(+), 146 deletions(-)

diff --git a/libs/@hashintel/petrinaut/src/components/button.tsx b/libs/@hashintel/petrinaut/src/components/button.tsx
index 6f554e9ef94..0fef0ad72d6 100644
--- a/libs/@hashintel/petrinaut/src/components/button.tsx
+++ b/libs/@hashintel/petrinaut/src/components/button.tsx
@@ -79,4 +79,4 @@ const ButtonBase: React.FC<ButtonProps> = ({
   </button>
 );
 
-export const Button = withTooltip(ButtonBase, "inline");
+export const Button = withTooltip(ButtonBase, "block");
diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
index 18847c32605..2c0a62d349b 100644
--- a/libs/@hashintel/petrinaut/src/components/code-editor.tsx
+++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx
@@ -1,9 +1,8 @@
-import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
 import { css, cva } from "@hashintel/ds-helpers/css";
 import type { EditorProps, Monaco } from "@monaco-editor/react";
 import MonacoEditor from "@monaco-editor/react";
-import type { editor, IDisposable } from "monaco-editor";
-import { useCallback, useEffect, useRef, useState } from "react";
+import type { editor } from "monaco-editor";
+import { useCallback, useRef } from "react";
 
 import { Tooltip } from "./tooltip";
 
@@ -25,16 +24,6 @@ const containerStyle = cva({
   },
 });
 
-const tooltipContentStyle = css({
-  backgroundColor: "gray.90",
-  color: "gray.10",
-  borderRadius: "md.6",
-  fontSize: "[13px]",
-  zIndex: "[10000]",
-  boxShadow: "[0 2px 8px rgba(0, 0, 0, 0.15)]",
-  padding: "[6px 10px]",
-});
-
 type CodeEditorProps = Omit<EditorProps, "theme"> & {
   tooltip?: string;
 };
@@ -53,61 +42,11 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
   ...props
 }) => {
   const isReadOnly = options?.readOnly === true;
-  const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false);
-  const [isHovering, setIsHovering] = useState(false);
-  const [isEditorMounted, setIsEditorMounted] = useState(false);
-  const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
   const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
-  const editAttemptListenerRef = useRef<IDisposable | null>(null);
-
-  // Cleanup timeout on unmount
-  useEffect(() => {
-    return () => {
-      if (hideTimeoutRef.current) {
-        clearTimeout(hideTimeoutRef.current);
-      }
-    };
-  }, []);
-
-  // Register/unregister edit attempt listener when isReadOnly changes or editor mounts
-  useEffect(() => {
-    // Dispose previous listener if exists
-    if (editAttemptListenerRef.current) {
-      editAttemptListenerRef.current.dispose();
-      editAttemptListenerRef.current = null;
-    }
-
-    // Register new listener if in read-only mode with tooltip and editor is mounted
-    if (isReadOnly && tooltip && editorRef.current) {
-      editAttemptListenerRef.current =
-        editorRef.current.onDidAttemptReadOnlyEdit(() => {
-          // Clear any existing timeout
-          if (hideTimeoutRef.current) {
-            clearTimeout(hideTimeoutRef.current);
-          }
-
-          // Show tooltip
-          setShowReadOnlyTooltip(true);
-
-          // Hide after 2 seconds
-          hideTimeoutRef.current = setTimeout(() => {
-            setShowReadOnlyTooltip(false);
-          }, 2000);
-        });
-    }
-
-    return () => {
-      if (editAttemptListenerRef.current) {
-        editAttemptListenerRef.current.dispose();
-        editAttemptListenerRef.current = null;
-      }
-    };
-  }, [isReadOnly, tooltip, isEditorMounted]);
 
   const handleMount = useCallback(
     (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => {
       editorRef.current = editorInstance;
-      setIsEditorMounted(true);
       // Call the original onMount if provided
       onMount?.(editorInstance, monaco);
     },
@@ -140,38 +79,20 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
     </div>
   );
 
-  // In read-only mode with tooltip, show on hover OR on edit attempt
-  if (isReadOnly && tooltip) {
-    const isTooltipOpen = isHovering || showReadOnlyTooltip;
-
+  // Regular tooltip for non-read-only mode (if tooltip is provided)
+  if (tooltip) {
     return (
-      <ArkTooltip.Root
-        open={isTooltipOpen}
-        openDelay={200}
-        closeDelay={0}
-        positioning={{ placement: "top" }}
+      <Tooltip
+        content={tooltip}
+        display="block"
+        className={css({
+          flex: "1",
+        })}
       >
-        <ArkTooltip.Trigger asChild>
-          <div
-            onMouseEnter={() => setIsHovering(true)}
-            onMouseLeave={() => setIsHovering(false)}
-          >
-            {editorElement}
-          </div>
-        </ArkTooltip.Trigger>
-        <ArkTooltip.Positioner>
-          <ArkTooltip.Content className={tooltipContentStyle}>
-            {tooltip}
-          </ArkTooltip.Content>
-        </ArkTooltip.Positioner>
-      </ArkTooltip.Root>
+        {editorElement}
+      </Tooltip>
     );
   }
 
-  // Regular tooltip for non-read-only mode (if tooltip is provided)
-  if (tooltip) {
-    return <Tooltip content={tooltip}>{editorElement}</Tooltip>;
-  }
-
   return editorElement;
 };
diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
index 2ebabc8e425..880bb925ce6 100644
--- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx
+++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx
@@ -1,6 +1,6 @@
 import { ark } from "@ark-ui/react/factory";
 import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip";
-import { css, cva } from "@hashintel/ds-helpers/css";
+import { css, cva, cx } from "@hashintel/ds-helpers/css";
 import type { SvgIconProps } from "@mui/material";
 import { SvgIcon, Tooltip as MuiTooltip } from "@mui/material";
 import type { FunctionComponent, ReactNode } from "react";
@@ -45,6 +45,10 @@ interface TooltipProps {
    * - "inline": For inline elements like buttons in flex containers
    */
   display?: "block" | "inline";
+  /**
+   * Optional className to apply to the trigger wrapper element.
+   */
+  className?: string;
 }
 
 /**
@@ -57,6 +61,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
   content,
   children,
   display = "block",
+  className,
 }) => {
   if (!content) {
     return children;
@@ -69,7 +74,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
       positioning={{ placement: "top" }}
     >
       <ArkTooltip.Trigger asChild>
-        <ark.span className={triggerWrapperStyle({ display })}>
+        <ark.span className={cx(triggerWrapperStyle({ display }), className)}>
           {children}
         </ark.span>
       </ArkTooltip.Trigger>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
index 299ba8184ac..018a4ca9b3d 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx
@@ -46,6 +46,7 @@ const typeDropdownButtonStyle = cva({
     fontSize: "[14px]",
     padding: "[6px 8px]",
     display: "flex",
+    justifyContent: "flex-start",
     gap: "[8px]",
     textAlign: "left",
   },
@@ -64,6 +65,10 @@ const colorDotStyle = css({
   flexShrink: 0,
 });
 
+const placeholderStyle = css({
+  color: "[rgba(0, 0, 0, 0.4)]",
+});
+
 const dropdownMenuStyle = css({
   position: "absolute",
   top: "[100%]",
@@ -304,7 +309,7 @@ export const DifferentialEquationProperties: React.FC<
             className={typeDropdownButtonStyle({ isDisabled: isReadOnly })}
             tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined}
           >
-            {associatedType && (
+            {associatedType ? (
               <>
                 <div
                   className={colorDotStyle}
@@ -312,45 +317,56 @@ export const DifferentialEquationProperties: React.FC<
                 />
                 <span>{associatedType.name}</span>
               </>
+            ) : (
+              <span className={placeholderStyle}>Select a type</span>
             )}
           </Button>
           {showTypeDropdown && !isReadOnly && (
             <div className={dropdownMenuStyle}>
-              {types.map((type) => (
-                <button
-                  key={type.id}
-                  type="button"
-                  onClick={() => {
-                    handleTypeChange(type.id);
-                    setShowTypeDropdown(false);
-                  }}
+              {types.length === 0 ? (
+                <div
                   className={dropdownItemStyle}
-                  style={{
-                    backgroundColor:
-                      type.id === differentialEquation.colorId
-                        ? "rgba(0, 0, 0, 0.05)"
-                        : "transparent",
-                  }}
-                  onMouseEnter={(event) => {
-                    // eslint-disable-next-line no-param-reassign
-                    event.currentTarget.style.backgroundColor =
-                      "rgba(0, 0, 0, 0.05)";
-                  }}
-                  onMouseLeave={(event) => {
-                    // eslint-disable-next-line no-param-reassign
-                    event.currentTarget.style.backgroundColor =
-                      type.id === differentialEquation.colorId
-                        ? "rgba(0, 0, 0, 0.05)"
-                        : "transparent";
-                  }}
+                  style={{ color: "rgba(0, 0, 0, 0.4)" }}
                 >
-                  <div
-                    className={colorDotStyle}
-                    style={{ backgroundColor: type.displayColor }}
-                  />
-                  <span>{type.name}</span>
-                </button>
-              ))}
+                  Create a type first
+                </div>
+              ) : (
+                types.map((type) => (
+                  <button
+                    key={type.id}
+                    type="button"
+                    onClick={() => {
+                      handleTypeChange(type.id);
+                      setShowTypeDropdown(false);
+                    }}
+                    className={dropdownItemStyle}
+                    style={{
+                      backgroundColor:
+                        type.id === differentialEquation.colorId
+                          ? "rgba(0, 0, 0, 0.05)"
+                          : "transparent",
+                    }}
+                    onMouseEnter={(event) => {
+                      // eslint-disable-next-line no-param-reassign
+                      event.currentTarget.style.backgroundColor =
+                        "rgba(0, 0, 0, 0.05)";
+                    }}
+                    onMouseLeave={(event) => {
+                      // eslint-disable-next-line no-param-reassign
+                      event.currentTarget.style.backgroundColor =
+                        type.id === differentialEquation.colorId
+                          ? "rgba(0, 0, 0, 0.05)"
+                          : "transparent";
+                    }}
+                  >
+                    <div
+                      className={colorDotStyle}
+                      style={{ backgroundColor: type.displayColor }}
+                    />
+                    <span>{type.name}</span>
+                  </button>
+                ))
+              )}
             </div>
           )}
         </div>
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
index dc1fa41e6eb..841431dc33f 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx
@@ -47,11 +47,23 @@ const dimensionsHintStyle = css({
   fontWeight: "normal",
 });
 
-const addDimensionButtonStyle = css({
-  fontSize: "[16px]",
-  padding: "[2px 8px]",
-  backgroundColor: "[rgba(59, 130, 246, 0.1)]",
-  color: "[#3b82f6]",
+const addDimensionButtonStyle = cva({
+  base: {
+    fontSize: "[16px]",
+    padding: "[2px 8px]",
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+        color: "[#999]",
+      },
+      false: {
+        backgroundColor: "[rgba(59, 130, 246, 0.1)]",
+        color: "[#3b82f6]",
+      },
+    },
+  },
 });
 
 const emptyDimensionsStyle = css({
@@ -147,17 +159,30 @@ const dimensionNameInputStyle = css({
   flex: "1",
 });
 
-const deleteDimensionButtonStyle = css({
-  fontSize: "[16px]",
-  width: "[28px]",
-  height: "[28px]",
-  borderRadius: "[3px]",
-  border: "[1px solid rgba(239, 68, 68, 0.2)]",
-  backgroundColor: "[rgba(239, 68, 68, 0.08)]",
-  color: "[#ef4444]",
-  fontWeight: "semibold",
-  lineHeight: "[1]",
-  flexShrink: 0,
+const deleteDimensionButtonStyle = cva({
+  base: {
+    fontSize: "[16px]",
+    width: "[28px]",
+    height: "[28px]",
+    borderRadius: "[3px]",
+    fontWeight: "semibold",
+    lineHeight: "[1]",
+    flexShrink: 0,
+  },
+  variants: {
+    isDisabled: {
+      true: {
+        border: "[1px solid rgba(0, 0, 0, 0.1)]",
+        backgroundColor: "[rgba(0, 0, 0, 0.05)]",
+        color: "[#999]",
+      },
+      false: {
+        border: "[1px solid rgba(239, 68, 68, 0.2)]",
+        backgroundColor: "[rgba(239, 68, 68, 0.08)]",
+        color: "[#ef4444]",
+      },
+    },
+  },
 });
 
 // --- Helpers ---
@@ -373,7 +398,7 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
           <Button
             onClick={handleAddElement}
             disabled={isDisabled}
-            className={addDimensionButtonStyle}
+            className={addDimensionButtonStyle({ isDisabled })}
             aria-label="Add dimension"
             tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
           >
@@ -443,7 +468,9 @@ export const TypeProperties: React.FC<TypePropertiesProps> = ({
                     handleDeleteElement(element.elementId, element.name);
                   }}
                   disabled={isDisabled || type.elements.length === 1}
-                  className={deleteDimensionButtonStyle}
+                  className={deleteDimensionButtonStyle({
+                    isDisabled: isDisabled || type.elements.length === 1,
+                  })}
                   aria-label={`Delete dimension ${element.name}`}
                   tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined}
                 >
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
index 1f823fd3cdc..eaaf74ae49b 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx
@@ -4,6 +4,7 @@ import { TbTrash } from "react-icons/tb";
 
 import { NumberInput } from "../../../components/number-input";
 import type { SubView } from "../../../components/sub-view/types";
+import { UI_MESSAGES } from "../../../constants/ui-messages";
 import { SimulationContext } from "../../../simulation/context";
 import { InitialStateEditor } from "../panels/PropertiesPanel/initial-state-editor";
 import { usePlacePropertiesContext } from "../panels/PropertiesPanel/place-properties-context";
@@ -117,6 +118,7 @@ const PlaceInitialStateContent: React.FC = () => {
             });
           }}
           disabled={hasSimulationFrames}
+          tooltip={hasSimulationFrames ? UI_MESSAGES.READ_ONLY_MODE : undefined}
         />
       </div>
     );
