From 289cf54e87b6b46bfb5d679a4d9bc446cd458d92 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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: `` - 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 --- .../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 & { + 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 = ({ + 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 = ( +
+ + {tooltip &&
} +
+ ); + + if (tooltip) { + return {editor}; + } + + 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 - * - * - * - */ -export const DisabledTooltip: React.FC = ({ - disabled, - children, - message = UI_MESSAGES.READ_ONLY_MODE, -}) => { - if (!disabled) { - return children; - } - - return {children}; -}; 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 = ({ checked, onCheckedChange, disabled = false, + tooltip, }) => { - return ( + const switchElement = ( { @@ -55,4 +59,10 @@ export const Switch: React.FC = ({ ); + + if (tooltip) { + return {switchElement}; + } + + 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 = ({ content, children }) => { + if (!content) { + return children; + } + return (
Name
- + - +
Associated Type
- + - + {showTypeDropdown && !isReadOnly && (
{types.map((type) => ( @@ -519,37 +498,22 @@ export const DifferentialEquationProperties: React.FC< /> )}
- -
- { - 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, - }} - /> -
-
+ { + 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} + />
); 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 = ({ {/* Name field */}
Name
- + = ({ disabled={isDisabled} className={inputStyle({ isDisabled })} /> - +
{/* Variable Name field */}
Variable Name
- + = ({ disabled={isDisabled} className={inputStyle({ isDisabled, isMonospace: true })} /> - +
{/* Type selector - hidden for now as internal code relies on "real" type */} @@ -173,7 +174,7 @@ export const ParameterProperties: React.FC = ({ {/* Default Value field */}
Default Value
- + = ({ disabled={isDisabled} className={inputStyle({ isDisabled, isMonospace: true })} /> - +
); 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 = ({
Name
- + = ({ disabled={isReadOnly} className={inputStyle({ isReadOnly, hasError: !!nameError })} /> - + {nameError &&
{nameError}
}
@@ -442,7 +443,9 @@ export const PlaceProperties: React.FC = ({ } 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).`} /> - + - +
{place.colorId && (
@@ -490,17 +493,16 @@ export const PlaceProperties: React.FC = ({
- - { - updatePlace(place.id, (existingPlace) => { - existingPlace.dynamicsEnabled = checked; - }); - }} - /> - + { + updatePlace(place.id, (existingPlace) => { + existingPlace.dynamicsEnabled = checked; + }); + }} + />
Dynamics @@ -523,7 +525,9 @@ export const PlaceProperties: React.FC = ({ availableDiffEqs.length > 0 && (
Differential Equation
- + - + {place.differentialEquationId && (
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 = ({
Name
- + = ({ disabled={isReadOnly} className={inputStyle({ isReadOnly })} /> - +
@@ -477,7 +453,7 @@ export const TransitionProperties: React.FC = ({ Firing time
- +
= ({ }} />
-
+
@@ -550,37 +526,22 @@ export const TransitionProperties: React.FC = ({ /> )}
- -
- `${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, - }} - /> -
-
+ `${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} + />
{/* Only show Transition Results if at least one output place has a type */} @@ -665,41 +626,24 @@ export const TransitionProperties: React.FC = ({ /> )}
- -
- `${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, - }} - /> -
-
+ `${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} + />
) : (
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 = ({
Name
- + = ({ disabled={isDisabled} className={inputStyle({ isDisabled })} /> - +
Color
- + { @@ -430,7 +431,7 @@ export const TypeProperties: React.FC = ({ }} disabled={isDisabled} /> - +
{/* Dimensions Section - Editable with drag-to-reorder */} @@ -440,7 +441,9 @@ export const TypeProperties: React.FC = ({ Dimensions (order matters)
- + - +
{type.elements.length === 0 ? ( @@ -489,7 +492,9 @@ export const TypeProperties: React.FC = ({
{index}
{/* Name input */} - + = ({ placeholder="dimension_name" className={dimensionNameInputStyle({ isDisabled })} /> - + {/* Delete button */} - + - +
))} From 9c737c24d74ab8d3d83b1cbf834a60ce4420be68 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ }} disabled={disabled} > - - + + ); if (tooltip) { - return {switchElement}; + // 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 ( + + {switchElement} + + ); } return switchElement; From 180d28851c4e3ecf158fc35c3f48079df5f90bb0 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ @@ -101,23 +129,30 @@ export const SegmentGroup: React.FC = ({ options, onChange, size = "md", + disabled = false, + tooltip, }) => { - return ( + const segmentGroupElement = ( { if (details.value) { onChange(details.value); } }} > -
+
{options.map((option) => ( {option.label} @@ -127,4 +162,15 @@ export const SegmentGroup: React.FC = ({
); + + if (tooltip) { + // Wrap in ark.span to properly forward tooltip event handlers + return ( + + {segmentGroupElement} + + ); + } + + 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 = ({ Firing time
- -
- { - updateTransition(transition.id, (existingTransition) => { - existingTransition.lambdaType = value as - | "predicate" - | "stochastic"; - }); - }} - /> -
-
+
+ { + updateTransition(transition.id, (existingTransition) => { + existingTransition.lambdaType = value as + | "predicate" + | "stochastic"; + }); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> +
From 16e658ecd077226da3e0e2c251694792cf194990 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ disabled = false, tooltip, }) => { + const triggerId = useId(); + const segmentGroupElement = ( = ({ } }} > -
+ {/* Use the container div as the tooltip trigger via shared ID */} +
{options.map((option) => ( = ({ ); 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 ( - - {segmentGroupElement} - + + {segmentGroupElement} + + + {tooltip} + + + ); } From 39448b5359c03c57f47bb7dc0d6c62010c7a38f2 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ disabled = false, tooltip, }) => { - const triggerId = useId(); - const segmentGroupElement = ( = ({ } }} > - {/* Use the container div as the tooltip trigger via shared ID */} -
+
{options.map((option) => ( = ({ ); 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 ( - - {segmentGroupElement} - - - {tooltip} - - - + + + {segmentGroupElement} + + ); } From 958fb41938acd105ceb468ea593124c25d328341 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ disabled = false, tooltip, }) => { - const segmentGroupElement = ( + const containerClassName = containerStyle({ size, isDisabled: disabled }); + + const containerContent = ( + <> + + {options.map((option) => ( + + {option.label} + + + + ))} + + ); + + if (tooltip) { + return ( + + { + if (details.value) { + onChange(details.value); + } + }} + > + {/* ArkTooltip.Trigger with asChild merges props with ark.div */} + + {containerContent} + + + + + {tooltip} + + + + ); + } + + return ( = ({ } }} > -
- - {options.map((option) => ( - - {option.label} - - - - ))} -
+
{containerContent}
); - - if (tooltip) { - // Use ark.span with display:contents to forward tooltip events - // without affecting layout - return ( - - - {segmentGroupElement} - - - ); - } - - return segmentGroupElement; }; From 1a70954b85ea718805a7be0cabc85ab572303c44 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({
Place
- + 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 = ({
Transition
- + From 57f8d65c6f6821abaa8a2186ea421c9c7bcc0e5f Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- 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 = ({ { updatePlace(place.id, (existingPlace) => { existingPlace.dynamicsEnabled = checked; From d13cd43c8c108258ec11c8b9974cfec6b01f6ca0 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({
{ if (checked) { // Turning on: use saved code if available, otherwise default From 303c763a920c8b7dd84e9d7cfcff512a7c85d101 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- 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 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 --- 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 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 --- .../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 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 --- .../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 (
@@ -155,24 +154,28 @@ const DifferentialEquationsSectionContent: React.FC = () => {
{eq.name}
- + +
); })} @@ -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 ( - + + + ); }; 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 ( - + + + ); }; @@ -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 (
@@ -250,22 +244,28 @@ const ParametersList: React.FC = () => { className={inputStyle} /> ) : ( - + + )}
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 (
@@ -208,24 +207,28 @@ const TypesSectionContent: React.FC = () => { style={{ backgroundColor: type.displayColor }} /> {type.name} - + +
); })} @@ -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 ( - + 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" + > + + + + ); }; From 6108b91c057f498eb7c20ef55f4e795fdd599ab5 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .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 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 --- 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 & { tooltip?: string; }; @@ -68,7 +62,6 @@ export const CodeEditor: React.FC = ({ options={editorOptions} {...props} /> - {tooltip &&
}
); From 1b95fc345369c48fbc1896b376e1c7bb1e97b044 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ closeDelay={0} positioning={{ placement: "top" }} > - { - if (details.value) { - onChange(details.value); - } - }} - > - {/* ArkTooltip.Trigger with asChild merges props with ark.div */} - - {containerContent} - - + + + { + if (details.value) { + onChange(details.value); + } + }} + > +
{containerContent}
+
+
+
{tooltip} From 4b5d66355df2c5162a74994e998d83dd1d27eac5 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ content, children }) => { if (!content) { return children; @@ -33,7 +44,10 @@ export const Tooltip: React.FC = ({ content, children }) => { closeDelay={0} positioning={{ placement: "top" }} > - {children} + {/* Wrapper span ensures tooltip works on disabled elements */} + + {children} + {content} From 5d3dd1945a29ee6719ae74500af971cc91bc2655 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- 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 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 --- 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 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 --- 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 = ({ content, children }) => { closeDelay={0} positioning={{ placement: "top" }} > - {/* Wrapper span ensures tooltip works on disabled elements */} + {/* Wrapper div ensures tooltip works on disabled elements */} - {children} +
{children}
From 83b22c862fc6011d0031a3b7c76ffc59147d7aad Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 = ({ content, children }) => { if (!content) { @@ -44,7 +44,7 @@ export const Tooltip: React.FC = ({ 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 */}
{children}
From 3620c95f97e62c0cc641f7216707198c11534990 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 & { tooltip?: string; }; @@ -30,15 +43,43 @@ type CodeEditorProps = Omit & { * 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 = ({ tooltip, options, height, + onMount, ...props }) => { const isReadOnly = options?.readOnly === true; + const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false); + const hideTimeoutRef = useRef | 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 = ({ theme="vs-light" height="100%" options={editorOptions} + onMount={handleMount} {...props} />
); + // In read-only mode with tooltip, use controlled tooltip that shows on edit attempts + if (isReadOnly && tooltip) { + return ( + + +
{editor}
+
+ + + {tooltip} + + +
+ ); + } + + // Regular tooltip for non-read-only mode (if tooltip is provided) if (tooltip) { return {editor}; } 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 = ({ ); - if (tooltip) { - return ( - - - - { - if (details.value) { - onChange(details.value); - } - }} - > -
{containerContent}
-
-
-
- - - {tooltip} - - -
- ); - } - - return ( + const segmentGroup = ( = ({
{containerContent}
); + + if (tooltip) { + return ( + + {segmentGroup} + + ); + } + + 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 = ({ ); 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 ( - - {switchElement} + + {switchElement} ); } 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 = ({ content, children }) => { +export const Tooltip: React.FC = ({ + content, + children, + display = "block", +}) => { if (!content) { return children; } @@ -44,9 +68,10 @@ export const Tooltip: React.FC = ({ content, children }) => { closeDelay={0} positioning={{ placement: "top" }} > - {/* Wrapper div with inline-flex preserves input dimensions while enabling tooltips on disabled elements */} -
{children}
+ + {children} +
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 = ({ }; return ( - +
+ * ); + * + * export const MyButton = withTooltip(MyButtonBase, "inline"); + * + * // Usage: + * + * ``` + */ +export function withTooltip

( + Component: ComponentType

, + defaultDisplay: "block" | "inline" = "block", +): ComponentType

{ + const WrappedComponent: React.FC

= ({ + tooltip, + tooltipDisplay = defaultDisplay, + ...props + }) => { + const element = ; + + if (!tooltip) { + return element; + } + + return ( + + {element} + + ); + }; + + // 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 = ({ +const SegmentGroupBase: React.FC = ({ value, options, onChange, size = "md", disabled = false, - tooltip, }) => { const containerClassName = containerStyle({ size, isDisabled: disabled }); - const containerContent = ( - <> - - {options.map((option) => ( - - {option.label} - - - - ))} - - ); - - const segmentGroup = ( + return ( = ({ } }} > -

{containerContent}
+
+ + {options.map((option) => ( + + {option.label} + + + + ))} +
); - - if (tooltip) { - return ( - - {segmentGroup} - - ); - } - - 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 = ({ +const SwitchBase: React.FC = ({ checked, onCheckedChange, disabled = false, - tooltip, -}) => { - const switchElement = ( - { - onCheckedChange?.(details.checked); - }} - disabled={disabled} - > - - - - - - ); +}) => ( + { + onCheckedChange?.(details.checked); + }} + disabled={disabled} + > + + + + + +); - if (tooltip) { - return ( - - {switchElement} - - ); - } - - return switchElement; -}; +export const Switch = withTooltip(SwitchBase, "inline"); From 1cc11b2035cddf2d86e41a256b7ac26b0beaf235 Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 { + /** Button variant */ + variant?: "default" | "ghost"; + /** Button content */ + children: React.ReactNode; + /** Ref to the button element */ + ref?: React.Ref; +} + +const ButtonBase: React.FC = ({ + variant = "default", + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +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 { + /** 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; +} + +const IconButtonBase: React.FC = ({ + size = "md", + variant = "default", + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +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, "type"> { + /** Whether to use monospace font */ + monospace?: boolean; + /** Whether the input has an error */ + hasError?: boolean; + /** Ref to the input element */ + ref?: React.Ref; +} + +const InputBase: React.FC = ({ + monospace = false, + hasError = false, + disabled, + className, + ref, + ...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, "type"> { + /** Whether the input has an error */ + hasError?: boolean; + /** Ref to the input element */ + ref?: React.Ref; +} + +const NumberInputBase: React.FC = ({ + hasError = false, + disabled, + className, + ref, + ...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 { + /** Ref to the select element */ + ref?: React.Ref; +} + +const SelectBase: React.FC = ({ + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +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, "type"> { + /** Ref to the input element */ + ref?: React.Ref; +} + +const SliderBase: React.FC = ({ className, ref, ...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<
Name
- - { - updateDifferentialEquation( - differentialEquation.id, - (existingEquation) => { - existingEquation.name = event.target.value; - }, - ); - }} - disabled={isReadOnly} - className={inputStyle({ isReadOnly })} - /> - + { + updateDifferentialEquation( + differentialEquation.id, + (existingEquation) => { + existingEquation.name = event.target.value; + }, + ); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
Associated Type
- setShowTypeDropdown(!showTypeDropdown)} + onBlur={() => setTimeout(() => setShowTypeDropdown(false), 200)} + disabled={isReadOnly} + className={typeDropdownButtonStyle({ isDisabled: isReadOnly })} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} > - - + {associatedType && ( + <> +
+ {associatedType.name} + + )} + {showTypeDropdown && !isReadOnly && (
{types.map((type) => ( @@ -429,20 +391,15 @@ export const DifferentialEquationProperties: React.FC< you want to continue?
- - +
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 = ({ {/* Name field */}
Name
- - - +
{/* Variable Name field */}
Variable Name
- - - +
{/* Type selector - hidden for now as internal code relies on "real" type */} @@ -174,15 +137,13 @@ export const ParameterProperties: React.FC = ({ {/* Default Value field */}
Default Value
- - - +
); 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 = ({
Place
- { + 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"} > - - + +
Name
- - { - 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 })} - /> - + { + 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 &&
{nameError}
}
@@ -462,49 +349,41 @@ export const PlaceProperties: React.FC = ({ } 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).`} />
- { + 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} > - - + + {types.map((type) => ( + + ))} + {place.colorId && (
- +
)}
@@ -550,42 +429,37 @@ export const PlaceProperties: React.FC = ({ availableDiffEqs.length > 0 && (
Differential Equation
- { + 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} > - - + + {availableDiffEqs.map((eq) => ( + + ))} + {place.differentialEquationId && (
- +
)}
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 = ({
{placeName}
weight - { @@ -163,13 +128,13 @@ export const SortableArcItem: React.FC = ({ onWeightChange(newWeight); } }} - className={weightInputStyle({ isDisabled: disabled })} + className={weightInputStyle} />
{onDelete && !disabled && ( - + )}
); 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 = ({
Transition
- { + 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"} > - - + +
Name
- - { - updateTransition(transition.id, (existingTransition) => { - existingTransition.name = event.target.value; - }); - }} - disabled={isReadOnly} - className={inputStyle({ isReadOnly })} - /> - + { + updateTransition(transition.id, (existingTransition) => { + existingTransition.name = event.target.value; + }); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
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 = ({
Name
- - { - updateType(type.id, (existingType) => { - existingType.name = event.target.value; - }); - }} - disabled={isDisabled} - className={inputStyle({ isDisabled })} - /> - + { + updateType(type.id, (existingType) => { + existingType.name = event.target.value; + }); + }} + disabled={isDisabled} + tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
@@ -441,20 +370,15 @@ export const TypeProperties: React.FC = ({ Dimensions (order matters)
- - - + + +
{type.elements.length === 0 ? ( @@ -493,47 +417,38 @@ export const TypeProperties: React.FC = ({
{index}
{/* Name input */} - - { - handleUpdateElementName( - element.elementId, - event.target.value, - ); - }} - onBlur={(event) => { - handleBlurElementName( - element.elementId, - event.target.value, - ); - }} - disabled={isDisabled} - placeholder="dimension_name" - className={dimensionNameInputStyle({ isDisabled })} - /> - + { + 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 */} - { + 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} > - - + × +
))}
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 (
Token count
- { const count = Math.max( @@ -143,7 +117,6 @@ const PlaceInitialStateContent: React.FC = () => { }); }} disabled={hasSimulationFrames} - className={inputStyle({ isDisabled: hasSimulationFrames })} />
); From 9b4418692875d03add9c7ef12552237d32379d6e Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- 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 { +type ButtonProps = React.ButtonHTMLAttributes & { /** Button variant */ variant?: "default" | "ghost"; /** Button content */ children: React.ReactNode; /** Ref to the button element */ ref?: React.Ref; -} +}; const ButtonBase: React.FC = ({ 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 { +type IconButtonProps = React.ButtonHTMLAttributes & { /** 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; -} +}; const IconButtonBase: React.FC = ({ 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, "type"> { +type InputProps = Omit, "type"> & { /** Whether to use monospace font */ monospace?: boolean; /** Whether the input has an error */ hasError?: boolean; /** Ref to the input element */ ref?: React.Ref; -} +}; const InputBase: React.FC = ({ 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, "type"> { +type NumberInputProps = Omit< + React.InputHTMLAttributes, + "type" +> & { /** Whether the input has an error */ hasError?: boolean; /** Ref to the input element */ ref?: React.Ref; -} +}; const NumberInputBase: React.FC = ({ 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 { +type SelectProps = React.SelectHTMLAttributes & { /** Ref to the select element */ ref?: React.Ref; -} +}; const SelectBase: React.FC = ({ 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, "type"> { +type SliderProps = Omit, "type"> & { /** Ref to the input element */ ref?: React.Ref; -} +}; const SliderBase: React.FC = ({ className, ref, ...props }) => ( 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 --- .../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 = ({ }) => { const isReadOnly = options?.readOnly === true; const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false); + const [isHovering, setIsHovering] = useState(false); const hideTimeoutRef = useRef | null>(null); + const editorRef = useRef(null); + const editAttemptListenerRef = useRef(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 = ({ 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 = ({ ...options, }; - const editor = ( + const editorElement = (
= ({
); - // 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 ( -
{editor}
+
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + {editorElement} +
@@ -130,8 +188,8 @@ export const CodeEditor: React.FC = ({ // Regular tooltip for non-read-only mode (if tooltip is provided) if (tooltip) { - return {editor}; + return {editorElement}; } - 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 = ({ { id: "generate-ai", label: ( - +
Generate with AI From 1f85b903957c26a7cbbb1db16d33c44b9c983f6c Mon Sep 17 00:00:00 2001 From: Chris Feijoo 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 --- .../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 = ({ const isReadOnly = options?.readOnly === true; const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false); const [isHovering, setIsHovering] = useState(false); + const [isEditorMounted, setIsEditorMounted] = useState(false); const hideTimeoutRef = useRef | null>(null); const editorRef = useRef(null); const editAttemptListenerRef = useRef(null); @@ -68,7 +69,7 @@ export const CodeEditor: React.FC = ({ }; }, []); - // 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 = ({ 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 = ({ 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 = ({ placeName, weight, disabled = false, + tooltip, onWeightChange, onDelete, }) => { @@ -122,6 +125,7 @@ export const SortableArcItem: React.FC = ({ 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 = ({ 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 = ({ 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 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 --- .../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 = ({ ); -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 & { tooltip?: string; }; @@ -53,61 +42,11 @@ export const CodeEditor: React.FC = ({ ...props }) => { const isReadOnly = options?.readOnly === true; - const [showReadOnlyTooltip, setShowReadOnlyTooltip] = useState(false); - const [isHovering, setIsHovering] = useState(false); - const [isEditorMounted, setIsEditorMounted] = useState(false); - const hideTimeoutRef = useRef | null>(null); const editorRef = useRef(null); - const editAttemptListenerRef = useRef(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 = ({
); - // 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 ( - - -
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - > - {editorElement} -
-
- - - {tooltip} - - -
+ {editorElement} +
); } - // Regular tooltip for non-read-only mode (if tooltip is provided) - if (tooltip) { - return {editorElement}; - } - 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 = ({ content, children, display = "block", + className, }) => { if (!content) { return children; @@ -69,7 +74,7 @@ export const Tooltip: React.FC = ({ positioning={{ placement: "top" }} > - + {children} 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 ? ( <>
{associatedType.name} + ) : ( + Select a type )} {showTypeDropdown && !isReadOnly && (
- {types.map((type) => ( - - ))} + Create a type first +
+ ) : ( + types.map((type) => ( + + )) + )}
)}
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 = ({
);