From 2b7d4e32dcbf317412aaadc23728c2aea8a9f324 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 16 Dec 2025 00:32:50 +0100 Subject: [PATCH 01/40] Everything always available in BottomBar --- .../components/BottomBar/bottom-bar.tsx | 8 +- .../BottomBar/simulation-controls.tsx | 299 +++++++++--------- 2 files changed, 161 insertions(+), 146 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index a06ea629ed2..031817b24aa 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -2,6 +2,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { refractive } from "@hashintel/refractive"; import { useEffect } from "react"; +import { useCheckerContext } from "../../../../state/checker-provider"; import { useEditorStore } from "../../../../state/editor-provider"; import type { EditorState } from "../../../../state/editor-store"; import { DiagnosticsIndicator } from "./diagnostics-indicator"; @@ -60,6 +61,9 @@ export const BottomBar: React.FC = ({ (state) => state.diagnosticsPanelHeight, ); + const { totalDiagnosticsCount } = useCheckerContext(); + const hasDiagnostics = totalDiagnosticsCount > 0; + // Fallback to 'pan' mode when switching to simulate mode if mutative mode useEffect(() => { if ( @@ -100,8 +104,8 @@ export const BottomBar: React.FC = ({ editionMode={editionMode} onEditionModeChange={onEditionModeChange} /> - - {mode === "simulate" && } +
+
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index ffe07e75b7e..8b5b602caab 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -6,7 +6,13 @@ import { Tooltip } from "../../../../components/tooltip"; import { FEATURE_FLAGS } from "../../../../feature-flags"; import { useSimulationStore } from "../../../../state/simulation-provider"; -export const SimulationControls: React.FC = () => { +interface SimulationControlsProps { + disabled?: boolean; +} + +export const SimulationControls: React.FC = ({ + disabled = false, +}) => { const simulation = useSimulationStore((state) => state.simulation); const simulationState = useSimulationStore((state) => state.state); const reset = useSimulationStore((state) => state.reset); @@ -15,12 +21,14 @@ export const SimulationControls: React.FC = () => { const pause = useSimulationStore((state) => state.pause); const dt = useSimulationStore((state) => state.dt); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame, + (state) => state.currentlyViewedFrame ); const setCurrentlyViewedFrame = useSimulationStore( - (state) => state.setCurrentlyViewedFrame, + (state) => state.setCurrentlyViewedFrame ); + const isDisabled = disabled; + const totalFrames = simulation?.frames.length ?? 0; const hasSimulation = simulation !== null; const isRunning = simulationState === "Running"; @@ -51,50 +59,161 @@ export const SimulationControls: React.FC = () => { }; return ( - <> -
-
+ {/* Record/Stop button - always visible */} + - {/* Record/Stop button - always visible */} - + {isRunning ? ( + + ) : FEATURE_FLAGS.RUNNING_MAN_ICON ? ( + + ) : ( + + )} + + + + {/* Frame controls - only visible when simulation exists */} + {hasSimulation && ( + <> + +
Frame
+
+ {currentlyViewedFrame + 1} / {totalFrames} +
+
+ {elapsedTime.toFixed(3)}s +
+
+ + setCurrentlyViewedFrame(Number(event.target.value)) + } + className={css({ + width: "[400px]", + height: "[4px]", + appearance: "none", + background: "core.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: "core.blue.50", + cursor: "pointer", + }, + "&::-moz-range-thumb": { + width: "[12px]", + height: "[12px]", + borderRadius: "[50%]", + background: "core.blue.50", + cursor: "pointer", + border: "none", + }, + })} + /> + + )} + + {/* Reset button - only visible when simulation exists */} + {hasSimulation && ( + - - {/* Frame controls - only visible when simulation exists */} - {hasSimulation && ( - <> - -
Frame
-
- {currentlyViewedFrame + 1} / {totalFrames} -
-
- {elapsedTime.toFixed(3)}s -
-
- - setCurrentlyViewedFrame(Number(event.target.value)) - } - className={css({ - width: "[400px]", - height: "[4px]", - appearance: "none", - background: "core.gray.30", - borderRadius: "[2px]", - outline: "none", - cursor: "pointer", - "&::-webkit-slider-thumb": { - appearance: "none", - width: "[12px]", - height: "[12px]", - borderRadius: "[50%]", - background: "core.blue.50", - cursor: "pointer", - }, - "&::-moz-range-thumb": { - width: "[12px]", - height: "[12px]", - borderRadius: "[50%]", - background: "core.blue.50", - cursor: "pointer", - border: "none", - }, - })} - /> - - )} - - {/* Reset button - only visible when simulation exists */} - {hasSimulation && ( - - - - )} -
- + )} +
); }; From dbd340d46048e942dbbda2115c7fafb337577f83 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 16 Dec 2025 00:36:53 +0100 Subject: [PATCH 02/40] Extract styles in SimulationControls --- .../BottomBar/simulation-controls.tsx | 205 +++++++++--------- 1 file changed, 105 insertions(+), 100 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 8b5b602caab..27f24ffe1ee 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -6,6 +6,105 @@ import { Tooltip } from "../../../../components/tooltip"; import { FEATURE_FLAGS } from "../../../../feature-flags"; import { useSimulationStore } from "../../../../state/simulation-provider"; +const containerStyle = css({ + display: "flex", + alignItems: "center", + padding: "[0 12px]", + gap: "[12px]", +}); + +const playPauseButtonStyle = css({ + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "[32px]", + height: "[32px]", + borderRadius: "[50%]", + border: "none", + background: "core.red.50", + color: "[white]", + fontSize: "[20px]", + transition: "[all 0.2s ease]", + "&:hover:not(:disabled)": { + background: "core.red.60", + transform: "[scale(1.05)]", + }, + "&:disabled": { + opacity: "[0.5]", + cursor: "not-allowed", + }, +}); + +const frameInfoStyle = css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + fontSize: "[11px]", + color: "core.gray.60", + fontWeight: "[500]", + minWidth: "[80px]", +}); + +const elapsedTimeStyle = css({ + fontSize: "[10px]", + color: "core.gray.50", + marginTop: "[2px]", +}); + +const sliderStyle = css({ + width: "[400px]", + height: "[4px]", + appearance: "none", + background: "core.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: "core.blue.50", + cursor: "pointer", + }, + "&::-moz-range-thumb": { + width: "[12px]", + height: "[12px]", + borderRadius: "[50%]", + background: "core.blue.50", + cursor: "pointer", + border: "none", + }, +}); + +const resetButtonStyle = css({ + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "[36px]", + height: "[36px]", + borderRadius: "[6px]", + border: "none", + background: "[transparent]", + color: "core.gray.80", + fontSize: "[18px]", + transition: "[all 0.2s ease]", + "&:hover:not(:disabled)": { + background: "core.gray.10", + transform: "[scale(1.05)]", + }, + "&:disabled": { + opacity: "[0.5]", + cursor: "not-allowed", + }, +}); + interface SimulationControlsProps { disabled?: boolean; } @@ -59,13 +158,7 @@ export const SimulationControls: React.FC = ({ }; return ( -
+
{/* Record/Stop button - always visible */} = ({ type="button" onClick={handlePlayPause} disabled={isDisabled} - className={css({ - cursor: "pointer", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[32px]", - height: "[32px]", - borderRadius: "[50%]", - border: "none", - background: "core.red.50", - color: "[white]", - fontSize: "[20px]", - transition: "[all 0.2s ease]", - "&:hover:not(:disabled)": { - background: "core.red.60", - transform: "[scale(1.05)]", - }, - "&:disabled": { - opacity: "[0.5]", - cursor: "not-allowed", - }, - })} + className={playPauseButtonStyle} aria-label={ simulationState === "NotRun" ? "Run simulation" @@ -125,30 +197,12 @@ export const SimulationControls: React.FC = ({ {/* Frame controls - only visible when simulation exists */} {hasSimulation && ( <> - +
Frame
{currentlyViewedFrame + 1} / {totalFrames}
-
- {elapsedTime.toFixed(3)}s -
+
{elapsedTime.toFixed(3)}s
= ({ onChange={(event) => setCurrentlyViewedFrame(Number(event.target.value)) } - className={css({ - width: "[400px]", - height: "[4px]", - appearance: "none", - background: "core.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: "core.blue.50", - cursor: "pointer", - }, - "&::-moz-range-thumb": { - width: "[12px]", - height: "[12px]", - borderRadius: "[50%]", - background: "core.blue.50", - cursor: "pointer", - border: "none", - }, - })} + className={sliderStyle} /> )} @@ -199,28 +225,7 @@ export const SimulationControls: React.FC = ({ type="button" onClick={handleReset} disabled={isDisabled} - className={css({ - cursor: "pointer", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[36px]", - height: "[36px]", - borderRadius: "[6px]", - border: "none", - background: "[transparent]", - color: "core.gray.80", - fontSize: "[18px]", - transition: "[all 0.2s ease]", - "&:hover:not(:disabled)": { - background: "core.gray.10", - transform: "[scale(1.05)]", - }, - "&:disabled": { - opacity: "[0.5]", - cursor: "not-allowed", - }, - })} + className={resetButtonStyle} aria-label="Reset simulation" > From 8084653c5b2cd9c6bd8f11ca78e0ab1c45514fd8 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 16 Dec 2025 00:40:29 +0100 Subject: [PATCH 03/40] Disable SimulateMode in ModeSelector for now --- .../views/Editor/components/mode-selector.tsx | 99 ++++++++++++------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx index d04099a395c..c780e3d98b2 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx @@ -2,6 +2,8 @@ import { SegmentGroup } from "@ark-ui/react/segment-group"; import { css, cva } from "@hashintel/ds-helpers/css"; import { refractive } from "@hashintel/refractive"; +import { Tooltip } from "../../../components/tooltip"; + export interface ModeSelectorProps { mode: "edit" | "simulate"; onChange: (mode: "edit" | "simulate") => void; @@ -32,19 +34,30 @@ const segmentIndicatorStyle = css({ zIndex: 0, }); -const segmentItemStyle = css({ - position: "relative", - zIndex: 1, - padding: "spacing.3", - fontSize: "size.textsm", - fontWeight: "medium", - cursor: "pointer", - transition: "[color 200ms]", - borderRadius: "[8px]", - display: "flex", - alignItems: "center", - justifyContent: "center", - minWidth: "[80px]", +const segmentItemStyle = cva({ + base: { + position: "relative", + zIndex: 1, + padding: "spacing.3", + fontSize: "size.textsm", + fontWeight: "medium", + cursor: "pointer", + transition: "[color 200ms]", + borderRadius: "[8px]", + display: "flex", + alignItems: "center", + justifyContent: "center", + minWidth: "[80px]", + }, + variants: { + disabled: { + true: { + cursor: "not-allowed", + opacity: "[0.5]", + pointerEvents: "none", + }, + }, + }, }); const segmentItemTextStyle = cva({ @@ -68,8 +81,8 @@ export const ModeSelector: React.FC = ({ onChange, }) => { const options = [ - { name: "Edit", value: "edit" }, - { name: "Simulate", value: "simulate" }, + { name: "Edit", value: "edit", disabled: false }, + { name: "Simulate", value: "simulate", disabled: true }, ]; return ( @@ -87,31 +100,51 @@ export const ModeSelector: React.FC = ({ value={mode} onValueChange={(details) => { if (details.value) { - onChange(details.value as "edit" | "simulate"); + const selectedOption = options.find( + (opt) => opt.value === details.value + ); + if (selectedOption && !selectedOption.disabled) { + onChange(details.value as "edit" | "simulate"); + } } }} className={segmentRootStyle} > - {options.map((option) => ( - // Ark UI uses string values; our mode union matches these option values. - - { + const item = ( + - {option.name} - - - - - ))} + + {option.name} + + + + + ); + + if (option.disabled) { + return ( + + {item} + + ); + } + + return item; + })}
From de12b662a8d850868dfb72ad371a8fd9f4f881ef Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 16 Dec 2025 00:48:46 +0100 Subject: [PATCH 04/40] Extract styles in LeftSidebar and make all sections always visible --- .../components/LeftSideBar/left-sidebar.tsx | 204 +++++++++++------- 1 file changed, 130 insertions(+), 74 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx index 0a45204c589..54e03855b57 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx @@ -1,4 +1,4 @@ -import { css } from "@hashintel/ds-helpers/css"; +import { css, cva } from "@hashintel/ds-helpers/css"; import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarRightCollapse, @@ -14,6 +14,125 @@ import { ParametersSection } from "./parameters-section"; import { SimulationStateSection } from "./simulation-state-section"; import { TypesSection } from "./types-section"; +const outerContainerStyle = cva({ + base: { + position: "fixed", + padding: "[12px]", + zIndex: 1000, + display: "flex", + }, + variants: { + isOpen: { + true: { + top: "[0]", + left: "[0]", + bottom: "[0]", + height: "[100%]", + }, + false: { + top: "[12px]", + left: "[12px]", + bottom: "[auto]", + height: "[auto]", + }, + }, + }, +}); + +const panelStyle = cva({ + base: { + borderRadius: "[12px]", + backgroundColor: "[rgba(255, 255, 255, 0.7)]", + boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", + border: "[1px solid rgba(255, 255, 255, 0.8)]", + backdropFilter: "[blur(12px)]", + position: "relative", + display: "flex", + }, + variants: { + isOpen: { + true: { + height: "[100%]", + width: "[320px]", + padding: "[16px]", + flexDirection: "column", + gap: "[16px]", + alignItems: "stretch", + }, + false: { + height: "auto", + width: "auto", + padding: "[8px 12px]", + flexDirection: "row", + gap: "[8px]", + alignItems: "center", + }, + }, + }, +}); + +const headerStyle = cva({ + base: { + display: "flex", + }, + variants: { + isOpen: { + true: { + flexDirection: "column", + gap: "[12px]", + paddingBottom: "[12px]", + borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", + alignItems: "stretch", + }, + false: { + flexDirection: "row", + gap: "[8px]", + paddingBottom: "[0]", + borderBottom: "none", + alignItems: "center", + }, + }, + }, +}); + +const headerInnerStyle = css({ + display: "flex", + alignItems: "center", + gap: "[8px]", +}); + +const titleContainerStyle = cva({ + base: {}, + variants: { + isOpen: { + true: { + flex: "[1]", + minWidth: "[0]", + }, + false: { + flex: "[0 0 auto]", + minWidth: "[120px]", + }, + }, + }, +}); + +const toggleButtonStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "spacing.1", + background: "[rgba(255, 255, 255, 0.5)]", + borderRadius: "radius.2", + cursor: "pointer", + flexShrink: 0, + width: "[28px]", + height: "[28px]", + _hover: { + backgroundColor: "[rgba(255, 255, 255, 0.8)]", + }, +}); + interface LeftSideBarProps { hideNetManagementControls: boolean; menuItems: MenuItem[]; @@ -32,69 +151,19 @@ export const LeftSideBar: React.FC = ({ title, onTitleChange, }) => { - const globalMode = useEditorStore((state) => state.globalMode); const isOpen = useEditorStore((state) => state.isLeftSidebarOpen); const setLeftSidebarOpen = useEditorStore( - (state) => state.setLeftSidebarOpen, + (state) => state.setLeftSidebarOpen ); return ( -
-
+
+
{/* Header with Menu, Title, and Toggle button */} -
-
+
+
-
+
{!hideNetManagementControls && ( = ({ type="button" onClick={() => setLeftSidebarOpen(!isOpen)} aria-label={isOpen ? "Collapse sidebar" : "Expand sidebar"} - className={css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "spacing.1", - background: "[rgba(255, 255, 255, 0.5)]", - borderRadius: "radius.2", - cursor: "pointer", - flexShrink: 0, - _hover: { - backgroundColor: "[rgba(255, 255, 255, 0.8)]", - }, - })} - style={{ width: 28, height: 28 }} + className={toggleButtonStyle} > {isOpen ? ( @@ -135,13 +191,13 @@ export const LeftSideBar: React.FC = ({ {isOpen && ( <> {/* Simulation State Section - only in Simulate mode */} - {globalMode === "simulate" && } + {/* Types Section - only in Edit mode */} - {globalMode === "edit" && } + {/* Differential Equations Section - only in Edit mode */} - {globalMode === "edit" && } + {/* Parameters Section */} From 381bbdd09d920b71493b231cf8d0ddf4bb46d55a Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 17 Dec 2025 11:05:26 +0100 Subject: [PATCH 05/40] Extract styles in PlaceNode --- .../src/views/SDCPN/components/place-node.tsx | 197 ++++++++++-------- 1 file changed, 107 insertions(+), 90 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx index c54391da297..cab31e7da57 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx @@ -1,4 +1,4 @@ -import { css } from "@hashintel/ds-helpers/css"; +import { css, cva } from "@hashintel/ds-helpers/css"; import { TbMathFunction } from "react-icons/tb"; import { Handle, type NodeProps, Position } from "reactflow"; @@ -8,6 +8,97 @@ import { useSimulationStore } from "../../../state/simulation-provider"; import type { PlaceNodeData } from "../../../state/types-for-editor-to-remove"; import { handleStyling } from "../styles/styling"; +const containerStyle = css({ + position: "relative", +}); + +const placeCircleStyle = cva({ + base: { + padding: "spacing.4", + borderRadius: "[50%]", + width: "[130px]", + height: "[130px]", + display: "flex", + justifyContent: "center", + alignItems: "center", + border: "2px solid", + fontSize: "[15px]", + boxSizing: "border-box", + position: "relative", + textAlign: "center", + lineHeight: "[1.3]", + cursor: "default", + transition: "[all 0.2s ease]", + _hover: { + boxShadow: "0 0 0 4px rgba(59, 130, 246, 0.1)", + }, + }, + variants: { + selection: { + resource: { + boxShadow: + "0 0 0 3px rgba(59, 178, 246, 0.4), 0 0 0 5px rgba(59, 190, 246, 0.2)", + }, + reactflow: { + boxShadow: + "0 0 0 4px rgba(249, 115, 22, 0.4), 0 0 0 6px rgba(249, 115, 22, 0.2)", + }, + none: {}, + }, + }, + defaultVariants: { + selection: "none", + }, +}); + +const dynamicsIconStyle = css({ + position: "absolute", + top: "[25px]", + left: "[0px]", + width: "[100%]", + display: "flex", + alignItems: "center", + gap: "spacing.4", + justifyContent: "center", + color: "core.blue.60", + fontSize: "[18px]", +}); + +const contentWrapperStyle = css({ + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "spacing.2", +}); + +const labelContainerStyle = css({ + textAlign: "center", + display: "flex", + flexWrap: "wrap", + justifyContent: "center", + padding: "[12px]", +}); + +const labelSegmentStyle = css({ + display: "inline-block", + whiteSpace: "nowrap", +}); + +const tokenCountBadgeStyle = css({ + position: "absolute", + top: "[70%]", + fontSize: "[11px]", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "[white]", + backgroundColor: "[black]", + width: "[20px]", + height: "[20px]", + borderRadius: "[50%]", + fontWeight: "semibold", +}); + export const PlaceNode: React.FC> = ({ id, data, @@ -15,14 +106,14 @@ export const PlaceNode: React.FC> = ({ selected, }: NodeProps) => { const isSimulateMode = useEditorStore( - (state) => state.globalMode === "simulate", + (state) => state.globalMode === "simulate" ); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, + (state) => state.selectedResourceId ); const simulation = useSimulationStore((state) => state.simulation); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame, + (state) => state.currentlyViewedFrame ); const initialMarking = useSimulationStore((state) => state.initialMarking); @@ -50,13 +141,14 @@ export const PlaceNode: React.FC> = ({ // Determine selection state const isSelectedByResource = selectedResourceId === id; + const selectionVariant = isSelectedByResource + ? "resource" + : selected + ? "reactflow" + : "none"; return ( -
+
> = ({ style={handleStyling} />
{data.dynamicsEnabled && ( -
+
)} -
-
+
+
{splitPascalCase(data.label).map((segment, index) => ( {segment} @@ -148,24 +182,7 @@ export const PlaceNode: React.FC> = ({
{tokenCount !== null && ( -
- {tokenCount} -
+
{tokenCount}
)}
From b14cf14cee3cde8ac639bd699d3fa86e800ceb80 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 17 Dec 2025 11:12:08 +0100 Subject: [PATCH 06/40] Make PlaceNode token count bigger --- .../petrinaut/src/views/SDCPN/components/place-node.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx index cab31e7da57..95819c988c6 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx @@ -87,14 +87,14 @@ const labelSegmentStyle = css({ const tokenCountBadgeStyle = css({ position: "absolute", top: "[70%]", - fontSize: "[11px]", + fontSize: "[16px]", display: "flex", alignItems: "center", justifyContent: "center", color: "[white]", backgroundColor: "[black]", - width: "[20px]", - height: "[20px]", + width: "[27px]", + height: "[27px]", borderRadius: "[50%]", fontWeight: "semibold", }); From 99e93dc465b71a0bbaaaa701aa31a4cfed80210a Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 17 Dec 2025 11:12:56 +0100 Subject: [PATCH 07/40] Make Simulation available in Edit Mode, and disable Simulate tab for now --- .../components/BottomPanel/bottom-panel.tsx | 200 ++++++ .../BottomPanel/diagnostics-content.tsx | 269 ++++++++ .../BottomPanel/parameters-content.tsx | 252 ++++++++ .../simulation-settings-content.tsx | 269 ++++++++ .../DiagnosticsPanel/diagnostics-panel.tsx | 402 ------------ .../components/LeftSideBar/left-sidebar.tsx | 8 - .../LeftSideBar/parameters-section.tsx | 258 -------- .../LeftSideBar/simulation-state-section.tsx | 227 ------- .../differential-equation-properties.tsx | 51 +- .../PropertiesPanel/place-properties.tsx | 610 +++++++++--------- .../src/views/Editor/editor-view.tsx | 12 +- 11 files changed, 1337 insertions(+), 1221 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx delete mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/DiagnosticsPanel/diagnostics-panel.tsx delete mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/parameters-section.tsx delete mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/simulation-state-section.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx new file mode 100644 index 00000000000..72d35710958 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -0,0 +1,200 @@ +import { css, cva } from "@hashintel/ds-helpers/css"; +import { useCallback, useRef, useState } from "react"; + +import { useEditorStore } from "../../../../state/editor-provider"; +import { DiagnosticsContent } from "./diagnostics-content"; +import { ParametersContent } from "./parameters-content"; +import { SimulationSettingsContent } from "./simulation-settings-content"; + +// Position offsets (accounting for sidebar padding/margins) +const MIN_HEIGHT = 100; +const MAX_HEIGHT = 600; +const LEFT_SIDEBAR_WIDTH = 344; // 320px + 24px padding +const PANEL_MARGIN = 12; + +type BottomPanelTab = "diagnostics" | "simulation-settings" | "parameters"; + +const panelContainerStyle = css({ + borderRadius: "[12px]", + backgroundColor: "[rgba(255, 255, 255, 0.7)]", + boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", + border: "[1px solid rgba(255, 255, 255, 0.8)]", + backdropFilter: "[blur(12px)]", + display: "flex", + flexDirection: "column", +}); + +const headerStyle = css({ + display: "flex", + alignItems: "center", + gap: "[4px]", + borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", + padding: "[4px 8px]", + flexShrink: 0, +}); + +const tabButtonStyle = cva({ + base: { + fontSize: "[12px]", + fontWeight: "[500]", + padding: "[6px 12px]", + borderRadius: "[6px]", + border: "none", + cursor: "pointer", + transition: "[all 0.15s ease]", + background: "[transparent]", + }, + variants: { + active: { + true: { + backgroundColor: "[rgba(0, 0, 0, 0.08)]", + color: "core.gray.90", + }, + false: { + color: "core.gray.60", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.04)]", + color: "core.gray.80", + }, + }, + }, + }, +}); + +const contentStyle = css({ + fontSize: "[12px]", + padding: "[8px 16px]", + flex: "[1]", + overflowY: "auto", +}); + +const tabs: { id: BottomPanelTab; label: string }[] = [ + { id: "diagnostics", label: "Diagnostics" }, + { id: "parameters", label: "Global Parameters" }, + { id: "simulation-settings", label: "Simulation Settings" }, +]; + +/** + * BottomPanel shows tabs for Diagnostics, Simulation Settings, and Parameters. + * Positioned at the bottom of the viewport. + * When LeftSideBar is visible, positioned to its right. Otherwise full-width. + * Resizable from the top edge. + */ +export const BottomPanel: React.FC = () => { + const isOpen = useEditorStore((state) => state.isDiagnosticsPanelOpen); + const isLeftSidebarOpen = useEditorStore((state) => state.isLeftSidebarOpen); + const panelHeight = useEditorStore((state) => state.diagnosticsPanelHeight); + const setDiagnosticsPanelHeight = useEditorStore( + (state) => state.setDiagnosticsPanelHeight + ); + + const [activeTab, setActiveTab] = useState("diagnostics"); + + // Resize handling + const resizeStartYRef = useRef(0); + const resizeStartHeightRef = useRef(panelHeight); + + const handleResizeMouseDown = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + resizeStartYRef.current = event.clientY; + resizeStartHeightRef.current = panelHeight; + + const handleMouseMove = (moveEvent: MouseEvent) => { + // Dragging up increases height (negative deltaY = increase) + const deltaY = resizeStartYRef.current - moveEvent.clientY; + const newHeight = Math.max( + MIN_HEIGHT, + Math.min(MAX_HEIGHT, resizeStartHeightRef.current + deltaY) + ); + setDiagnosticsPanelHeight(newHeight); + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [panelHeight, setDiagnosticsPanelHeight] + ); + + if (!isOpen) { + return null; + } + + // Calculate left position based on left sidebar state + const leftOffset = isLeftSidebarOpen ? LEFT_SIDEBAR_WIDTH : PANEL_MARGIN; + + const renderContent = () => { + switch (activeTab) { + case "diagnostics": + return ; + case "simulation-settings": + return ; + case "parameters": + return ; + } + }; + + return ( +
+ {/* Resize handle at top */} + + ))} +
+ + {/* Scrollable content */} +
{renderContent()}
+
+ ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx new file mode 100644 index 00000000000..7ded983e520 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx @@ -0,0 +1,269 @@ +import { css } from "@hashintel/ds-helpers/css"; +import { useCallback, useMemo, useState } from "react"; +import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; +import ts from "typescript"; + +import { useCheckerContext } from "../../../../state/checker-provider"; +import { useEditorStore } from "../../../../state/editor-provider"; +import { useSDCPNContext } from "../../../../state/sdcpn-provider"; + +/** + * Formats a TypeScript diagnostic message to a readable string + */ +const formatDiagnosticMessage = ( + messageText: string | ts.DiagnosticMessageChain +): string => { + if (typeof messageText === "string") { + return messageText; + } + return ts.flattenDiagnosticMessageText(messageText, "\n"); +}; + +const emptyMessageStyle = css({ + color: "core.gray.50", + fontStyle: "italic", +}); + +const entityButtonStyle = css({ + fontSize: "[12px]", + fontWeight: "medium", + color: "core.gray.80", + "&:hover": { + color: "core.gray.90", + }, +}); + +const errorCountStyle = css({ + color: "[#dc2626]", + fontWeight: "normal", +}); + +const subTypeStyle = css({ + fontSize: "[11px]", + fontWeight: "medium", + color: "core.gray.60", +}); + +const diagnosticsListStyle = css({ + margin: "[0]", + listStyle: "none", +}); + +const diagnosticButtonStyle = css({ + fontSize: "[11px]", + fontFamily: + "[ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace]", + color: "[#dc2626]", + lineHeight: "[1.5]", + cursor: "pointer", + borderRadius: "[4px]", + transition: "[background-color 0.15s]", + backgroundColor: "[transparent]", + border: "none", + textAlign: "left", + width: "[100%]", + "&:hover": { + backgroundColor: "[rgba(220, 38, 38, 0.08)]", + }, +}); + +const positionStyle = css({ + color: "core.gray.50", +}); + +type EntityType = "transition" | "differential-equation"; + +interface GroupedDiagnostics { + entityType: EntityType; + entityId: string; + entityName: string; + errorCount: number; + items: Array<{ + subType: "lambda" | "kernel" | null; + diagnostics: ts.Diagnostic[]; + }>; +} + +/** + * DiagnosticsContent shows the full list of diagnostics grouped by entity. + */ +export const DiagnosticsContent: React.FC = () => { + const { checkResult, totalDiagnosticsCount } = useCheckerContext(); + const { petriNetDefinition } = useSDCPNContext(); + const setSelectedResourceId = useEditorStore( + (state) => state.setSelectedResourceId + ); + const [expandedEntities, setExpandedEntities] = useState>( + new Set() + ); + + // Handler to select an entity when clicking on a diagnostic + const handleSelectEntity = useCallback( + (entityId: string) => { + setSelectedResourceId(entityId); + }, + [setSelectedResourceId] + ); + + // Group diagnostics by entity (transition or differential equation) + const groupedDiagnostics = useMemo(() => { + const groups = new Map(); + + for (const item of checkResult.itemDiagnostics) { + const entityId = item.itemId; + let entityType: EntityType; + let entityName: string; + let subType: "lambda" | "kernel" | null; + + if (item.itemType === "differential-equation") { + entityType = "differential-equation"; + const de = petriNetDefinition.differentialEquations.find( + (deItem) => deItem.id === entityId + ); + entityName = de?.name ?? entityId; + subType = null; + } else { + entityType = "transition"; + const transition = petriNetDefinition.transitions.find( + (tr) => tr.id === entityId + ); + entityName = transition?.name ?? entityId; + subType = item.itemType === "transition-lambda" ? "lambda" : "kernel"; + } + + const key = `${entityType}:${entityId}`; + if (!groups.has(key)) { + groups.set(key, { + entityType, + entityId, + entityName, + errorCount: 0, + items: [], + }); + } + + const group = groups.get(key)!; + group.errorCount += item.diagnostics.length; + group.items.push({ + subType, + diagnostics: item.diagnostics, + }); + } + + return Array.from(groups.values()); + }, [checkResult, petriNetDefinition]); + + const toggleEntity = useCallback((entityKey: string) => { + setExpandedEntities((prev) => { + const next = new Set(prev); + if (next.has(entityKey)) { + next.delete(entityKey); + } else { + next.add(entityKey); + } + return next; + }); + }, []); + + if (totalDiagnosticsCount === 0) { + return
No diagnostics
; + } + + return ( + <> + {groupedDiagnostics.map((group) => { + const entityKey = `${group.entityType}:${group.entityId}`; + const isExpanded = expandedEntities.has(entityKey); + const entityLabel = + group.entityType === "transition" + ? `Transition: ${group.entityName}` + : `Differential Equation: ${group.entityName}`; + + return ( +
+ {/* Collapsible entity header */} + + + {/* Expanded diagnostics */} + {isExpanded && ( +
+ {group.items.map((itemGroup) => ( +
+ {/* Show sub-type for transitions */} + {itemGroup.subType && ( +
+ {itemGroup.subType === "lambda" ? "Lambda" : "Kernel"} +
+ )} + + {/* Diagnostics list */} +
    + {itemGroup.diagnostics.map((diagnostic, index) => ( +
  • + +
  • + ))} +
+
+ ))} +
+ )} +
+ ); + })} + + ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx new file mode 100644 index 00000000000..7438c629211 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx @@ -0,0 +1,252 @@ +import { css } from "@hashintel/ds-helpers/css"; +import { v4 as uuidv4 } from "uuid"; + +import { InfoIconTooltip } from "../../../../components/tooltip"; +import { useEditorStore } from "../../../../state/editor-provider"; +import { useSDCPNContext } from "../../../../state/sdcpn-provider"; +import { useSimulationStore } from "../../../../state/simulation-provider"; + +const headerStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + marginBottom: "[12px]", +}); + +const titleStyle = css({ + display: "flex", + alignItems: "center", + gap: "[6px]", + fontWeight: 600, + fontSize: "[13px]", + color: "[#333]", +}); + +const addButtonStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "spacing.1", + borderRadius: "radius.2", + cursor: "pointer", + fontSize: "[18px]", + color: "core.gray.60", + background: "[transparent]", + border: "none", + width: "[24px]", + height: "[24px]", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + color: "core.gray.90", + }, +}); + +const parameterRowStyle = css({ + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.03)]", + }, +}); + +const parameterRowSelectedStyle = css({ + _hover: { + backgroundColor: "[rgba(59, 130, 246, 0.2)]", + }, +}); + +const inputStyle = css({ + padding: "[2px 6px]", + fontSize: "[12px]", + borderRadius: "radius.2", + border: "1px solid", + borderColor: "core.gray.30", + backgroundColor: "[white]", + width: "[80px]", + textAlign: "right", + _focus: { + outline: "none", + borderColor: "core.blue.50", + }, +}); + +const deleteButtonStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "spacing.1", + borderRadius: "radius.2", + cursor: "pointer", + fontSize: "[14px]", + color: "core.gray.50", + background: "[transparent]", + border: "none", + width: "[20px]", + height: "[20px]", + _hover: { + backgroundColor: "[rgba(255, 0, 0, 0.1)]", + color: "[#ef4444]", + }, +}); + +const emptyMessageStyle = css({ + fontSize: "[13px]", + color: "[#9ca3af]", + padding: "spacing.4", + textAlign: "center", +}); + +/** + * ParametersContent displays global parameters in the BottomPanel. + */ +export const ParametersContent: React.FC = () => { + const { + petriNetDefinition: { parameters }, + addParameter, + removeParameter, + } = useSDCPNContext(); + const globalMode = useEditorStore((state) => state.globalMode); + const simulationState = useSimulationStore((state) => state.state); + const selectedResourceId = useEditorStore( + (state) => state.selectedResourceId, + ); + const setSelectedResourceId = useEditorStore( + (state) => state.setSelectedResourceId, + ); + const parameterValues = useSimulationStore((state) => state.parameterValues); + const setParameterValue = useSimulationStore( + (state) => state.setParameterValue, + ); + + const isSimulationNotRun = + globalMode === "simulate" && simulationState === "NotRun"; + const isSimulationMode = globalMode === "simulate"; + + const handleAddParameter = () => { + const name = `param${parameters.length + 1}`; + const id = uuidv4(); + addParameter({ + id, + name: `Parameter ${parameters.length + 1}`, + variableName: name, + type: "real", + defaultValue: "0", + }); + setSelectedResourceId(id); + }; + + return ( +
+
+
+ Global Parameters + +
+ {!isSimulationMode && ( + + )} +
+ +
+ {parameters.map((param) => { + const isSelected = selectedResourceId === param.id; + + return ( +
{ + // Don't trigger selection if clicking the delete button or input + if ( + event.target instanceof HTMLElement && + (event.target.closest("button[aria-label^='Delete']") || + event.target.closest("input")) + ) { + return; + } + setSelectedResourceId(param.id); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + setSelectedResourceId(param.id); + } + }} + style={{ + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "4px 2px 4px 8px", + fontSize: 13, + borderRadius: 4, + backgroundColor: isSelected + ? "rgba(59, 130, 246, 0.15)" + : "#f9fafb", + cursor: "pointer", + }} + className={ + isSelected ? parameterRowSelectedStyle : parameterRowStyle + } + > +
+
{param.name}
+
+                  {param.variableName}
+                
+
+
+ {isSimulationMode ? ( + + setParameterValue(param.variableName, event.target.value) + } + placeholder={param.defaultValue} + readOnly={!isSimulationNotRun} + className={inputStyle} + /> + ) : ( + + )} +
+
+ ); + })} + {parameters.length === 0 && ( +
No global parameters yet
+ )} +
+
+ ); +}; + + diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx new file mode 100644 index 00000000000..7af189d88e5 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -0,0 +1,269 @@ +import { css } from "@hashintel/ds-helpers/css"; +import { useState } from "react"; +import { TbArrowRight } from "react-icons/tb"; + +import { useEditorStore } from "../../../../state/editor-provider"; +import { useSimulationStore } from "../../../../state/simulation-provider"; + +const sectionStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[12px]", +}); + +const settingsContainerStyle = css({ + display: "flex", + flexDirection: "row", + gap: "[24px]", + flexWrap: "wrap", +}); + +const settingGroupStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[4px]", + minWidth: "[150px]", +}); + +const labelStyle = css({ + fontSize: "[12px]", + fontWeight: "[500]", + color: "[rgba(0, 0, 0, 0.7)]", +}); + +const smallLabelStyle = css({ + fontSize: "[10px]", + fontWeight: "[400]", +}); + +const inputStyle = css({ + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + backgroundColor: "[white]", + width: "[120px]", +}); + +const inputDisabledStyle = css({ + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + cursor: "not-allowed", + width: "[120px]", +}); + +const selectStyle = css({ + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + backgroundColor: "[white]", + cursor: "pointer", + width: "[120px]", +}); + +const selectDisabledStyle = css({ + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + cursor: "not-allowed", + width: "[120px]", +}); + +const stateContainerStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[4px]", + padding: "[8px 12px]", + backgroundColor: "[rgba(0, 0, 0, 0.03)]", + borderRadius: "[4px]", +}); + +const stateLabelStyle = css({ + fontSize: "[11px]", + fontWeight: "[600]", + textTransform: "uppercase", + color: "[rgba(0, 0, 0, 0.5)]", + letterSpacing: "[0.5px]", +}); + +const errorTextStyle = css({ + fontSize: "[11px]", + color: "[#d32f2f]", + marginTop: "[4px]", + maxWidth: "[400px]", + wordWrap: "break-word", + userSelect: "text", + cursor: "text", + textWrap: "wrap", +}); + +const editButtonStyle = css({ + fontSize: "[11px]", + padding: "[4px 8px]", + border: "[1px solid rgba(211, 47, 47, 0.3)]", + borderRadius: "[4px]", + backgroundColor: "[white]", + cursor: "pointer", + color: "[#d32f2f]", + display: "inline-flex", + alignItems: "center", + gap: "[4px]", + marginTop: "[4px]", + alignSelf: "flex-start", +}); + +const resetButtonStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + background: "[transparent]", + border: "1px solid", + borderColor: "core.gray.30", + borderRadius: "radius.4", + cursor: "pointer", + color: "core.gray.70", + fontSize: "[12px]", + fontWeight: "[500]", + padding: "[4px 12px]", + alignSelf: "flex-start", + _hover: { + backgroundColor: "core.gray.10", + borderColor: "core.gray.40", + }, +}); + +const getStateColor = (state: string): string => { + switch (state) { + case "Running": + return "#1976d2"; + case "Complete": + return "#2e7d32"; + case "Error": + return "#d32f2f"; + case "Paused": + return "#ed6c02"; + default: + return "rgba(0, 0, 0, 0.7)"; + } +}; + +/** + * SimulationSettingsContent displays simulation settings in the BottomPanel. + */ +export const SimulationSettingsContent: React.FC = () => { + const setGlobalMode = useEditorStore((state) => state.setGlobalMode); + const simulationState = useSimulationStore((state) => state.state); + const simulationError = useSimulationStore((state) => state.error); + const errorItemId = useSimulationStore((state) => state.errorItemId); + const reset = useSimulationStore((state) => state.reset); + const dt = useSimulationStore((state) => state.dt); + const setDt = useSimulationStore((state) => state.setDt); + const setSelectedResourceId = useEditorStore( + (state) => state.setSelectedResourceId + ); + + // Local state for ODE solver (not used in simulation yet, but UI is ready) + const [odeSolver, setOdeSolver] = useState("euler"); + + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + + return ( +
+ {/* Settings Row */} +
+ {/* Time Step Input */} +
+ + { + const value = Number.parseFloat(event.target.value); + if (value > 0) { + setDt(value); + } + }} + disabled={isSimulationActive} + className={isSimulationActive ? inputDisabledStyle : inputStyle} + /> +
+ + {/* ODE Solver Method Select */} +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- it can't tell it's the same ID */} + + +
+ + {/* Simulation State */} +
+ Simulation State + + {simulationState === "NotRun" ? "Not Started" : simulationState} + +
+ + {/* Reset Button */} + {simulationState !== "NotRun" && ( + + )} +
+ + {/* Error Display */} + {simulationState === "Error" && simulationError && ( +
+
{simulationError}
+ {errorItemId && ( + + )} +
+ )} +
+ ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/DiagnosticsPanel/diagnostics-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/DiagnosticsPanel/diagnostics-panel.tsx deleted file mode 100644 index 9487c8cc1cd..00000000000 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/DiagnosticsPanel/diagnostics-panel.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import { css } from "@hashintel/ds-helpers/css"; -import { useCallback, useMemo, useRef, useState } from "react"; -import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; -import ts from "typescript"; - -import { useCheckerContext } from "../../../../state/checker-provider"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; - -/** - * Formats a TypeScript diagnostic message to a readable string - */ -function formatDiagnosticMessage( - messageText: string | ts.DiagnosticMessageChain, -): string { - if (typeof messageText === "string") { - return messageText; - } - return ts.flattenDiagnosticMessageText(messageText, "\n"); -} - -// Position offsets (accounting for sidebar padding/margins) -const MIN_HEIGHT = 100; -const MAX_HEIGHT = 600; -const LEFT_SIDEBAR_WIDTH = 344; // 320px + 24px padding -const PANEL_MARGIN = 12; - -// Styles -const panelContainerStyle = css({ - borderRadius: "[12px]", - backgroundColor: "[rgba(255, 255, 255, 0.7)]", - boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", - border: "[1px solid rgba(255, 255, 255, 0.8)]", - backdropFilter: "[blur(12px)]", - display: "flex", - flexDirection: "column", -}); - -const headerStyle = css({ - fontSize: "[12px]", - fontWeight: "[500]", - textTransform: "uppercase", - color: "core.gray.90", - borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", -}); - -const contentStyle = css({ - fontSize: "[12px]", -}); - -const emptyMessageStyle = css({ - color: "core.gray.50", - fontStyle: "italic", -}); - -const entityButtonStyle = css({ - fontSize: "[12px]", - fontWeight: "medium", - color: "core.gray.80", - "&:hover": { - color: "core.gray.90", - }, -}); - -const errorCountStyle = css({ - color: "[#dc2626]", - fontWeight: "normal", -}); - -const subTypeStyle = css({ - fontSize: "[11px]", - fontWeight: "medium", - color: "core.gray.60", -}); - -const diagnosticsListStyle = css({ - margin: "[0]", - listStyle: "none", -}); - -const diagnosticButtonStyle = css({ - fontSize: "[11px]", - fontFamily: - "[ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace]", - color: "[#dc2626]", - lineHeight: "[1.5]", - cursor: "pointer", - borderRadius: "[4px]", - transition: "[background-color 0.15s]", - backgroundColor: "[transparent]", - border: "none", - textAlign: "left", - width: "[100%]", - "&:hover": { - backgroundColor: "[rgba(220, 38, 38, 0.08)]", - }, -}); - -const positionStyle = css({ - color: "core.gray.50", -}); - -type EntityType = "transition" | "differential-equation"; - -interface GroupedDiagnostics { - entityType: EntityType; - entityId: string; - entityName: string; - errorCount: number; - items: Array<{ - subType: "lambda" | "kernel" | null; - diagnostics: ts.Diagnostic[]; - }>; -} - -/** - * DiagnosticsPanel shows the full list of diagnostics. - * Positioned at the bottom of the viewport. - * When LeftSideBar is visible, positioned to its right. Otherwise full-width. - * Resizable from the top edge. - */ -export const DiagnosticsPanel: React.FC = () => { - const { checkResult, totalDiagnosticsCount } = useCheckerContext(); - const { petriNetDefinition } = useSDCPNContext(); - const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, - ); - const isOpen = useEditorStore((state) => state.isDiagnosticsPanelOpen); - const isLeftSidebarOpen = useEditorStore((state) => state.isLeftSidebarOpen); - const panelHeight = useEditorStore((state) => state.diagnosticsPanelHeight); - const setDiagnosticsPanelHeight = useEditorStore( - (state) => state.setDiagnosticsPanelHeight, - ); - const [expandedEntities, setExpandedEntities] = useState>( - new Set(), - ); - - // Handler to select an entity when clicking on a diagnostic - const handleSelectEntity = useCallback( - (entityId: string) => { - setSelectedResourceId(entityId); - }, - [setSelectedResourceId], - ); - - // Group diagnostics by entity (transition or differential equation) - const groupedDiagnostics = useMemo(() => { - const groups = new Map(); - - for (const item of checkResult.itemDiagnostics) { - const entityId = item.itemId; - let entityType: EntityType; - let entityName: string; - let subType: "lambda" | "kernel" | null; - - if (item.itemType === "differential-equation") { - entityType = "differential-equation"; - const de = petriNetDefinition.differentialEquations.find( - (deItem) => deItem.id === entityId, - ); - entityName = de?.name ?? entityId; - subType = null; - } else { - entityType = "transition"; - const transition = petriNetDefinition.transitions.find( - (tr) => tr.id === entityId, - ); - entityName = transition?.name ?? entityId; - subType = item.itemType === "transition-lambda" ? "lambda" : "kernel"; - } - - const key = `${entityType}:${entityId}`; - if (!groups.has(key)) { - groups.set(key, { - entityType, - entityId, - entityName, - errorCount: 0, - items: [], - }); - } - - const group = groups.get(key)!; - group.errorCount += item.diagnostics.length; - group.items.push({ - subType, - diagnostics: item.diagnostics, - }); - } - - return Array.from(groups.values()); - }, [checkResult, petriNetDefinition]); - - const toggleEntity = useCallback((entityKey: string) => { - setExpandedEntities((prev) => { - const next = new Set(prev); - if (next.has(entityKey)) { - next.delete(entityKey); - } else { - next.add(entityKey); - } - return next; - }); - }, []); - - // Resize handling - const resizeStartYRef = useRef(0); - const resizeStartHeightRef = useRef(panelHeight); - - const handleResizeMouseDown = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - resizeStartYRef.current = event.clientY; - resizeStartHeightRef.current = panelHeight; - - const handleMouseMove = (moveEvent: MouseEvent) => { - // Dragging up increases height (negative deltaY = increase) - const deltaY = resizeStartYRef.current - moveEvent.clientY; - const newHeight = Math.max( - MIN_HEIGHT, - Math.min(MAX_HEIGHT, resizeStartHeightRef.current + deltaY), - ); - setDiagnosticsPanelHeight(newHeight); - }; - - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }, - [panelHeight, setDiagnosticsPanelHeight], - ); - - if (!isOpen) { - return null; - } - - // Calculate left position based on left sidebar state - const leftOffset = isLeftSidebarOpen ? LEFT_SIDEBAR_WIDTH : PANEL_MARGIN; - - return ( -
- {/* Resize handle at top */} - - - {/* Expanded diagnostics */} - {isExpanded && ( -
- {group.items.map((itemGroup) => ( -
- {/* Show sub-type for transitions */} - {itemGroup.subType && ( -
- {itemGroup.subType === "lambda" - ? "Lambda" - : "Kernel"} -
- )} - - {/* Diagnostics list */} -
    - {itemGroup.diagnostics.map((diagnostic, index) => ( - // eslint-disable-next-line react/no-array-index-key -
  • - -
  • - ))} -
-
- ))} -
- )} -
- ); - }) - )} -
-
- ); -}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx index 54e03855b57..ab6f86d0ace 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx @@ -10,8 +10,6 @@ import { DifferentialEquationsSection } from "./differential-equations-section"; import { FloatingTitle } from "./floating-title"; import { HamburgerMenu } from "./hamburger-menu"; import { NodesSection } from "./nodes-section"; -import { ParametersSection } from "./parameters-section"; -import { SimulationStateSection } from "./simulation-state-section"; import { TypesSection } from "./types-section"; const outerContainerStyle = cva({ @@ -190,18 +188,12 @@ export const LeftSideBar: React.FC = ({ {/* Content sections - only visible when open */} {isOpen && ( <> - {/* Simulation State Section - only in Simulate mode */} - - {/* Types Section - only in Edit mode */} {/* Differential Equations Section - only in Edit mode */} - {/* Parameters Section */} - - {/* Nodes Section */} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/parameters-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/parameters-section.tsx deleted file mode 100644 index d9eeaca3721..00000000000 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/parameters-section.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { css } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; -import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; -import { v4 as uuidv4 } from "uuid"; - -import { InfoIconTooltip } from "../../../../components/tooltip"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; - -export const ParametersSection: React.FC = () => { - const [isExpanded, setIsExpanded] = useState(true); - const { - petriNetDefinition: { parameters }, - addParameter, - removeParameter, - } = useSDCPNContext(); - const globalMode = useEditorStore((state) => state.globalMode); - const simulationState = useSimulationStore((state) => state.state); - const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, - ); - const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, - ); - const parameterValues = useSimulationStore((state) => state.parameterValues); - const setParameterValue = useSimulationStore( - (state) => state.setParameterValue, - ); - - const isSimulationNotRun = - globalMode === "simulate" && simulationState === "NotRun"; - const isSimulationMode = globalMode === "simulate"; - - return ( -
-
- - {!isSimulationMode && ( - - )} -
- {isExpanded && ( -
- {parameters.map((param) => { - const isSelected = selectedResourceId === param.id; - - return ( -
{ - // Don't trigger selection if clicking the delete button or input - if ( - event.target instanceof HTMLElement && - (event.target.closest("button[aria-label^='Delete']") || - event.target.closest("input")) - ) { - return; - } - setSelectedResourceId(param.id); - }} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - setSelectedResourceId(param.id); - } - }} - style={{ - width: "100%", - display: "flex", - alignItems: "center", - justifyContent: "space-between", - padding: "4px 2px 4px 8px", - fontSize: 13, - borderRadius: 4, - backgroundColor: isSelected - ? "rgba(59, 130, 246, 0.15)" - : "#f9fafb", - cursor: "pointer", - }} - className={css({ - _hover: { - backgroundColor: isSelected - ? "[rgba(59, 130, 246, 0.2)]" - : "[rgba(0, 0, 0, 0.05)]", - }, - })} - > -
-
{param.name}
-
{param.variableName}
-
-
- {isSimulationMode ? ( - - setParameterValue( - param.variableName, - event.target.value, - ) - } - placeholder={param.defaultValue} - readOnly={!isSimulationNotRun} - className={css({ - padding: "[2px 6px]", - fontSize: "[12px]", - borderRadius: "radius.2", - border: "1px solid", - borderColor: "core.gray.30", - backgroundColor: "[white]", - width: "[80px]", - textAlign: "right", - _focus: { - outline: "none", - borderColor: "core.blue.50", - }, - })} - /> - ) : ( - - )} -
-
- ); - })} - {parameters.length === 0 && ( -
- No global parameters yet -
- )} -
- )} -
- ); -}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/simulation-state-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/simulation-state-section.tsx deleted file mode 100644 index acad92dc4f3..00000000000 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/simulation-state-section.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { css } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; -import { TbArrowRight } from "react-icons/tb"; - -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; - -export const SimulationStateSection: React.FC = () => { - const setGlobalMode = useEditorStore((state) => state.setGlobalMode); - const simulationState = useSimulationStore((state) => state.state); - const simulationError = useSimulationStore((state) => state.error); - const errorItemId = useSimulationStore((state) => state.errorItemId); - const reset = useSimulationStore((state) => state.reset); - const dt = useSimulationStore((state) => state.dt); - const setDt = useSimulationStore((state) => state.setDt); - const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, - ); - - // Local state for ODE solver (not used in simulation yet, but UI is ready) - const [odeSolver, setOdeSolver] = useState("euler"); - - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - - return ( -
-
-
- - Simulation State - - - {simulationState === "NotRun" ? "Not Started" : simulationState} - - {simulationState === "Error" && simulationError && ( - <> -
-                {simulationError}
-              
- {errorItemId && ( - - )} - - )} -
- - {/* Time Step Input */} -
- - { - const value = Number.parseFloat(event.target.value); - if (value > 0) { - setDt(value); - } - }} - disabled={isSimulationActive} - style={{ - fontSize: 14, - padding: "6px 8px", - border: "1px solid rgba(0, 0, 0, 0.1)", - borderRadius: 4, - backgroundColor: isSimulationActive - ? "rgba(0, 0, 0, 0.05)" - : "white", - cursor: isSimulationActive ? "not-allowed" : "text", - }} - /> -
- - {/* ODE Solver Method Select */} -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- it can't tell it's the same ID */} - - -
-
- - {simulationState !== "NotRun" && ( - - )} -
- ); -}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx index a624056f371..75d759feddb 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx @@ -16,6 +16,7 @@ import type { DifferentialEquation, Place, } from "../../../../core/types/sdcpn"; +import { useSimulationStore } from "../../../../state/simulation-provider"; interface DifferentialEquationPropertiesProps { differentialEquation: DifferentialEquation; @@ -24,7 +25,7 @@ interface DifferentialEquationPropertiesProps { globalMode: "edit" | "simulate"; updateDifferentialEquation: ( equationId: string, - updateFn: (equation: DifferentialEquation) => void, + updateFn: (equation: DifferentialEquation) => void ) => void; } @@ -41,8 +42,13 @@ export const DifferentialEquationProperties: React.FC< const [pendingTypeId, setPendingTypeId] = useState(null); const [showTypeDropdown, setShowTypeDropdown] = useState(false); + const simulationState = useSimulationStore((state) => state.state); + const isSimulationRunning = + simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = globalMode === "simulate" || isSimulationRunning; + const associatedType = types.find( - (type) => type.id === differentialEquation.colorId, + (type) => type.id === differentialEquation.colorId ); // Find places that use this differential equation @@ -68,7 +74,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.colorId = newTypeId; - }, + } ); } }; @@ -79,7 +85,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.colorId = pendingTypeId; - }, + } ); } setShowConfirmDialog(false); @@ -118,10 +124,10 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.name = event.target.value; - }, + } ); }} - disabled={globalMode === "simulate"} + disabled={isReadOnly} style={{ fontSize: 14, padding: "6px 8px", @@ -129,9 +135,8 @@ export const DifferentialEquationProperties: React.FC< borderRadius: 4, width: "100%", boxSizing: "border-box", - backgroundColor: - globalMode === "simulate" ? "rgba(0, 0, 0, 0.05)" : "white", - cursor: globalMode === "simulate" ? "not-allowed" : "text", + backgroundColor: isReadOnly ? "rgba(0, 0, 0, 0.05)" : "white", + cursor: isReadOnly ? "not-allowed" : "text", }} />
@@ -145,16 +150,15 @@ export const DifferentialEquationProperties: React.FC< type="button" onClick={() => setShowTypeDropdown(!showTypeDropdown)} onBlur={() => setTimeout(() => setShowTypeDropdown(false), 200)} - disabled={globalMode === "simulate"} + disabled={isReadOnly} style={{ width: "100%", fontSize: 14, padding: "6px 8px", border: "1px solid rgba(0, 0, 0, 0.1)", borderRadius: 4, - backgroundColor: - globalMode === "simulate" ? "rgba(0, 0, 0, 0.05)" : "white", - cursor: globalMode === "simulate" ? "not-allowed" : "pointer", + backgroundColor: isReadOnly ? "rgba(0, 0, 0, 0.05)" : "white", + cursor: isReadOnly ? "not-allowed" : "pointer", display: "flex", alignItems: "center", gap: 8, @@ -176,7 +180,7 @@ export const DifferentialEquationProperties: React.FC< )} - {showTypeDropdown && globalMode === "edit" && ( + {showTypeDropdown && !isReadOnly && (
Code
- {globalMode === "edit" && ( + {!isReadOnly && ( { // Get the associated type to generate appropriate default code const equationType = types.find( - (t) => t.id === differentialEquation.colorId, + (t) => t.id === differentialEquation.colorId ); updateDifferentialEquation( @@ -396,10 +400,10 @@ export const DifferentialEquationProperties: React.FC< (existingEquation) => { existingEquation.code = equationType ? generateDefaultDifferentialEquationCode( - equationType, + equationType ) : DEFAULT_DIFFERENTIAL_EQUATION_CODE; - }, + } ); }, }, @@ -435,11 +439,8 @@ export const DifferentialEquationProperties: React.FC< overflow: "hidden", flex: 1, minHeight: 0, - filter: - globalMode === "simulate" - ? "grayscale(20%) brightness(98%)" - : "none", - pointerEvents: globalMode === "simulate" ? "none" : "auto", + filter: isReadOnly ? "grayscale(20%) brightness(98%)" : "none", + pointerEvents: isReadOnly ? "none" : "auto", }} > { existingEquation.code = newCode ?? ""; - }, + } ); }} path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`} @@ -466,7 +467,7 @@ export const DifferentialEquationProperties: React.FC< lineNumbersMinChars: 3, padding: { top: 8, bottom: 8 }, fixedOverflowWidgets: true, - readOnly: globalMode === "simulate", + readOnly: isReadOnly, }} />
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx index 8adcc185029..2c83957aa7e 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx @@ -48,21 +48,18 @@ export const PlaceProperties: React.FC = ({ globalMode, updatePlace, }) => { - const isSimulationNotRun = useSimulationStore( - (state) => state.state === "NotRun", - ); const simulation = useSimulationStore((state) => state.simulation); const initialMarking = useSimulationStore((state) => state.initialMarking); const setInitialMarking = useSimulationStore( - (state) => state.setInitialMarking, + (state) => state.setInitialMarking ); const parameterValues = useSimulationStore((state) => state.parameterValues); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame, + (state) => state.currentlyViewedFrame ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); const { @@ -130,7 +127,7 @@ export const PlaceProperties: React.FC = ({ if (!isPascalCase(nameInputValue)) { setNameError( - "Name must be in PascalCase (e.g., MyPlaceName or Place2). Any numbers must appear at the end.", + "Name must be in PascalCase (e.g., MyPlaceName or Place2). Any numbers must appear at the end." ); return; } @@ -191,7 +188,7 @@ export const PlaceProperties: React.FC = ({ 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.`, + `Are you sure you want to delete "${place.name}"? All arcs connected to this place will also be removed.` ) ) { removePlace(place.id); @@ -271,7 +268,11 @@ export const PlaceProperties: React.FC = ({
Accepted token type
{ + const count = Math.max( + 0, + Number.parseInt(event.target.value, 10) || 0 + ); + setInitialMarking(place.id, { + values: new Float64Array(0), // Empty array for places without type + count, + }); + }} + disabled={hasSimulationFrames} style={{ - fontWeight: 500, - fontSize: 12, - marginBottom: 4, + fontSize: 14, + padding: "6px 8px", + border: "1px solid rgba(0, 0, 0, 0.1)", + borderRadius: 4, + width: "100%", + boxSizing: "border-box", + backgroundColor: hasSimulationFrames + ? "rgba(0, 0, 0, 0.05)" + : "white", + cursor: hasSimulationFrames ? "not-allowed" : "text", }} - > - {isSimulationNotRun ? "Initial State" : "State"} -
-
-
- Token count -
- { - const count = Math.max( - 0, - Number.parseInt(event.target.value, 10) || 0, - ); - setInitialMarking(place.id, { - values: new Float64Array(0), // Empty array for places without type - count, - }); - }} - disabled={hasSimulation} - style={{ - fontSize: 14, - padding: "6px 8px", - border: "1px solid rgba(0, 0, 0, 0.1)", - borderRadius: 4, - width: "100%", - boxSizing: "border-box", - backgroundColor: hasSimulation - ? "rgba(0, 0, 0, 0.05)" - : "white", - cursor: hasSimulation ? "not-allowed" : "text", - }} - /> -
+ />
- ); - } - - return ( - +
); - })()} + } + + return ( + + ); + })()} {/* Visualizer section */} {globalMode === "edit" && ( @@ -588,225 +586,245 @@ export const PlaceProperties: React.FC = ({ {place.visualizerCode !== undefined && (
-
-
- {globalMode === "simulate" - ? "Visualizer Output" - : "Visualizer Code"} -
- {globalMode === "edit" && ( - - - - } - items={[ - { - id: "load-default", - label: "Load default template", - onClick: () => { - // Get the place's type to generate appropriate default code - const placeType = place.colorId - ? types.find((t) => t.id === place.colorId) - : null; + {(() => { + // Determine if we should show visualization (when simulation has frames) + const hasSimulationFrames = + simulation !== null && simulation.frames.length > 0; + const showVisualization = + globalMode === "simulate" || hasSimulationFrames; - updatePlace(place.id, (existingPlace) => { - existingPlace.visualizerCode = placeType - ? generateDefaultVisualizerCode(placeType) - : DEFAULT_VISUALIZER_CODE; - }); - }, - }, - { - id: "generate-ai", - label: ( - -
+
+
+ {showVisualization + ? "Visualizer Output" + : "Visualizer Code"} +
+ {!showVisualization && ( + - - Generate with AI -
- - ), - disabled: true, - onClick: () => { - // TODO: Implement AI generation - }, - }, - ]} - /> - )} -
-
- {globalMode === "simulate" ? ( - // Show live token values and parameters during simulation - (() => { - // Get place type to determine dimensions - const placeType = place.colorId - ? types.find((tp) => tp.id === place.colorId) - : null; - - if (!placeType) { - return ( -
- Place has no type set -
- ); - } - - const dimensions = placeType.elements.length; - const tokens: Record[] = []; - let parameters: Record = {}; - - // Check if we have simulation frames or use initial marking - if (simulation && simulation.frames.length > 0) { - // Use currently viewed simulation frame - const currentFrame = simulation.frames[currentlyViewedFrame]; - if (!currentFrame) { - return ( -
- No frame data available -
- ); - } - - const placeState = currentFrame.places.get(place.id); - if (!placeState) { - return ( -
- Place not found in frame -
- ); - } - - const { offset, count } = placeState; - const placeSize = count * dimensions; - const tokenValues = Array.from( - currentFrame.buffer.slice(offset, offset + placeSize), - ); - - // Format tokens as array of objects with named dimensions - for (let i = 0; i < count; i++) { - const token: Record = {}; - for (let colIndex = 0; colIndex < dimensions; colIndex++) { - const dimensionName = placeType.elements[colIndex]!.name; - token[dimensionName] = - tokenValues[i * dimensions + colIndex] ?? 0; - } - tokens.push(token); - } + + + } + items={[ + { + id: "load-default", + label: "Load default template", + onClick: () => { + // Get the place's type to generate appropriate default code + const placeType = place.colorId + ? types.find((t) => t.id === place.colorId) + : null; + + updatePlace(place.id, (existingPlace) => { + existingPlace.visualizerCode = placeType + ? generateDefaultVisualizerCode(placeType) + : DEFAULT_VISUALIZER_CODE; + }); + }, + }, + { + id: "generate-ai", + label: ( + +
+ + Generate with AI +
+
+ ), + disabled: true, + onClick: () => { + // TODO: Implement AI generation + }, + }, + ]} + /> + )} +
+
+ {showVisualization ? ( + // Show live token values and parameters during simulation + (() => { + // Get place type to determine dimensions + const placeType = place.colorId + ? types.find((tp) => tp.id === place.colorId) + : null; - // Merge SimulationStore values with SDCPN defaults - parameters = mergeParameterValues( - parameterValues, - defaultParameterValues, - ); - } else { - // Use initial marking - const marking = initialMarking.get(place.id); - if (marking && marking.count > 0) { - for (let i = 0; i < marking.count; i++) { - const token: Record = {}; - for ( - let colIndex = 0; - colIndex < dimensions; - colIndex++ - ) { - const dimensionName = - placeType.elements[colIndex]!.name; - token[dimensionName] = - marking.values[i * dimensions + colIndex] ?? 0; + if (!placeType) { + return ( +
+ Place has no type set +
+ ); } - tokens.push(token); - } - } - // Merge SimulationStore values with SDCPN defaults - parameters = mergeParameterValues( - parameterValues, - defaultParameterValues, - ); - } + const dimensions = placeType.elements.length; + const tokens: Record[] = []; + let parameters: Record = {}; + + // Check if we have simulation frames or use initial marking + if (simulation && simulation.frames.length > 0) { + // Use currently viewed simulation frame + const currentFrame = + simulation.frames[currentlyViewedFrame]; + if (!currentFrame) { + return ( +
+ No frame data available +
+ ); + } + + const placeState = currentFrame.places.get(place.id); + if (!placeState) { + return ( +
+ Place not found in frame +
+ ); + } + + const { offset, count } = placeState; + const placeSize = count * dimensions; + const tokenValues = Array.from( + currentFrame.buffer.slice(offset, offset + placeSize) + ); + + // Format tokens as array of objects with named dimensions + for (let i = 0; i < count; i++) { + const token: Record = {}; + for ( + let colIndex = 0; + colIndex < dimensions; + colIndex++ + ) { + const dimensionName = + placeType.elements[colIndex]!.name; + token[dimensionName] = + tokenValues[i * dimensions + colIndex] ?? 0; + } + tokens.push(token); + } + + // Merge SimulationStore values with SDCPN defaults + parameters = mergeParameterValues( + parameterValues, + defaultParameterValues + ); + } else { + // Use initial marking + const marking = initialMarking.get(place.id); + if (marking && marking.count > 0) { + for (let i = 0; i < marking.count; i++) { + const token: Record = {}; + for ( + let colIndex = 0; + colIndex < dimensions; + colIndex++ + ) { + const dimensionName = + placeType.elements[colIndex]!.name; + token[dimensionName] = + marking.values[i * dimensions + colIndex] ?? 0; + } + tokens.push(token); + } + } + + // Merge SimulationStore values with SDCPN defaults + parameters = mergeParameterValues( + parameterValues, + defaultParameterValues + ); + } - // Render the compiled visualizer component - if (!VisualizerComponent) { - return ( -
- Failed to compile visualizer code. Check console for - errors. -
- ); - } + // Render the compiled visualizer component + if (!VisualizerComponent) { + return ( +
+ Failed to compile visualizer code. Check console for + errors. +
+ ); + } - return ( - - + + + ); + })() + ) : ( + // Show code editor in edit mode + { + updatePlace(place.id, (existingPlace) => { + existingPlace.visualizerCode = 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 }, + fixedOverflowWidgets: true, + }} /> - - ); - })() - ) : ( - // Show code editor in edit mode - { - updatePlace(place.id, (existingPlace) => { - existingPlace.visualizerCode = 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 }, - fixedOverflowWidgets: true, - }} - /> - )} -
+ )} +
+ + ); + })()}
)} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx index 00f06ea9faa..858b9336c7c 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx @@ -10,7 +10,7 @@ import { useSDCPNContext } from "../../state/sdcpn-provider"; import { useSimulationStore } from "../../state/simulation-provider"; import { SDCPNView } from "../SDCPN/sdcpn-view"; import { BottomBar } from "./components/BottomBar/bottom-bar"; -import { DiagnosticsPanel } from "./components/DiagnosticsPanel/diagnostics-panel"; +import { BottomPanel } from "./components/BottomPanel/bottom-panel"; import { LeftSideBar } from "./components/LeftSideBar/left-sidebar"; import { ModeSelector } from "./components/mode-selector"; import { PropertiesPanel } from "./components/PropertiesPanel/properties-panel"; @@ -23,7 +23,9 @@ import { importSDCPN } from "./lib/import-sdcpn"; */ export const EditorView = ({ hideNetManagementControls, -}: { hideNetManagementControls: boolean }) => { +}: { + hideNetManagementControls: boolean; +}) => { // Get data from sdcpn-store const { createNewNet, @@ -44,7 +46,7 @@ export const EditorView = ({ // Get simulation store method to initialize parameter values const initializeParameterValuesFromDefaults = useSimulationStore( - (state) => state.initializeParameterValuesFromDefaults, + (state) => state.initializeParameterValuesFromDefaults ); // Handler for mode change that initializes parameter values when switching to simulate mode @@ -224,8 +226,8 @@ export const EditorView = ({ {/* SDCPN Visualization */} - {/* Diagnostics Panel - Bottom of viewport */} - + {/* Bottom Panel - Diagnostics, Simulation Settings, Parameters */} + Date: Wed, 17 Dec 2025 11:20:14 +0100 Subject: [PATCH 08/40] Fix BrokenMachines example --- libs/@hashintel/petrinaut/src/examples/broken-machines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/examples/broken-machines.ts b/libs/@hashintel/petrinaut/src/examples/broken-machines.ts index c4151a30f6d..eb7d487342b 100644 --- a/libs/@hashintel/petrinaut/src/examples/broken-machines.ts +++ b/libs/@hashintel/petrinaut/src/examples/broken-machines.ts @@ -303,7 +303,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } = lambdaType: "predicate", lambdaCode: "export default Lambda(() => true)", transitionKernelCode: - "/**\n* This function defines the kernel for the transition.\n* It receives tokens from input places,\n* and any global parameters defined,\n* and should return tokens for output places keyed by place name.\n*/\nexport default TransitionKernel((tokens) => {\n // tokens is an object which looks like:\n // { PlaceA: [{ x: 0, y: 0 }], PlaceB: [...] }\n // where 'x' and 'y' are examples of dimensions (properties)\n // of the token's type.\n\n // Return an object with output place names as keys\n return {\n MachinesBeingRepaired: [\n { machine_damage_ratio: tokens.BrokenMachines[0].machine_damage_ratio }\n ],\n };\n});", + "/**\n* This function defines the kernel for the transition.\n* It receives tokens from input places,\n* and any global parameters defined,\n* and should return tokens for output places keyed by place name.\n*/\nexport default TransitionKernel((tokens) => {\n // tokens is an object which looks like:\n // { PlaceA: [{ x: 0, y: 0 }], PlaceB: [...] }\n // where 'x' and 'y' are examples of dimensions (properties)\n // of the token's type.\n\n // Return an object with output place names as keys\n return {\n MachinesBeingRepaired: [\n { machine_damage_ratio: tokens.MachinesToRepair[0].machine_damage_ratio }\n ],\n };\n});", x: 1635, y: 480, width: 160, From e52e6af04f0fccbd92144ae5c17080c546b8e82b Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 17 Dec 2025 11:25:24 +0100 Subject: [PATCH 09/40] Wording --- .../views/Editor/components/BottomBar/simulation-controls.tsx | 2 +- .../Editor/components/BottomPanel/diagnostics-content.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 27f24ffe1ee..09facfe7fda 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -163,7 +163,7 @@ export const SimulationControls: React.FC = ({ { }, []); if (totalDiagnosticsCount === 0) { - return
No diagnostics
; + return ( +
No errors detected in your model
+ ); } return ( From e721aa92e91d1b608b114f1af88be5bd93e2a51c Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 00:20:02 +0100 Subject: [PATCH 10/40] Create GlassPanel component --- .../petrinaut/src/components/glass-panel.tsx | 74 +++++++++++++++++++ .../components/BottomPanel/bottom-panel.tsx | 12 +-- .../components/LeftSideBar/left-sidebar.tsx | 13 +--- .../PropertiesPanel/properties-panel.tsx | 67 +++++------------ 4 files changed, 102 insertions(+), 64 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/components/glass-panel.tsx diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx new file mode 100644 index 00000000000..4b3c71d0b81 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -0,0 +1,74 @@ +import { css, cx } from "@hashintel/ds-helpers/css"; +import type { CSSProperties, ReactNode } from "react"; + +const panelContainerStyle = css({ + position: "relative", + borderRadius: "[12px]", + backgroundColor: "[rgba(255, 255, 255, 0.7)]", + boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", + border: "[1px solid rgba(255, 255, 255, 0.8)]", + overflow: "hidden", +}); + +const blurOverlayStyle = css({ + position: "absolute", + inset: "[0]", + borderRadius: "[12px]", + pointerEvents: "none", + backdropFilter: "[blur(24px)]", +}); + +const contentContainerStyle = css({ + position: "relative", + height: "[100%]", + width: "[100%]", +}); + +interface GlassPanelProps { + /** Content to render inside the panel */ + children: ReactNode; + /** Additional CSS class name for the panel container */ + className?: string; + /** Inline styles for the panel container */ + style?: CSSProperties; + /** Additional CSS class name for the content container */ + contentClassName?: string; + /** Inline styles for the content container */ + contentStyle?: CSSProperties; + /** Blur amount in pixels (default: 24) */ + blur?: number; +} + +/** + * GlassPanel provides a frosted glass-like appearance with backdrop blur. + * + * Uses a separate overlay element for the backdrop-filter to avoid + * interfering with child components that use fixed/absolute positioning + * (e.g., Monaco Editor hover widgets). + */ +export const GlassPanel: React.FC = ({ + children, + className, + style, + contentClassName, + contentStyle, + blur = 24, +}) => { + return ( +
+ {/* Blur overlay - separate from content to avoid affecting child positioning */} +
+ + {/* Content container */} +
+ {children} +
+
+ ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 72d35710958..7c05f155a9b 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -1,6 +1,7 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { useCallback, useRef, useState } from "react"; +import { GlassPanel } from "../../../../components/glass-panel"; import { useEditorStore } from "../../../../state/editor-provider"; import { DiagnosticsContent } from "./diagnostics-content"; import { ParametersContent } from "./parameters-content"; @@ -15,11 +16,6 @@ const PANEL_MARGIN = 12; type BottomPanelTab = "diagnostics" | "simulation-settings" | "parameters"; const panelContainerStyle = css({ - borderRadius: "[12px]", - backgroundColor: "[rgba(255, 255, 255, 0.7)]", - boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", - border: "[1px solid rgba(255, 255, 255, 0.8)]", - backdropFilter: "[blur(12px)]", display: "flex", flexDirection: "column", }); @@ -140,7 +136,7 @@ export const BottomPanel: React.FC = () => { }; return ( -
{ zIndex: 999, padding: 4, }} - className={panelContainerStyle} + contentClassName={panelContainerStyle} > {/* Resize handle at top */}
+ ); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx index ab6f86d0ace..9ee2b64f9b9 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx @@ -4,6 +4,7 @@ import { TbLayoutSidebarRightCollapse, } from "react-icons/tb"; +import { GlassPanel } from "../../../../components/glass-panel"; import type { MenuItem } from "../../../../components/menu"; import { useEditorStore } from "../../../../state/editor-provider"; import { DifferentialEquationsSection } from "./differential-equations-section"; @@ -37,14 +38,8 @@ const outerContainerStyle = cva({ }, }); -const panelStyle = cva({ +const panelContentStyle = cva({ base: { - borderRadius: "[12px]", - backgroundColor: "[rgba(255, 255, 255, 0.7)]", - boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", - border: "[1px solid rgba(255, 255, 255, 0.8)]", - backdropFilter: "[blur(12px)]", - position: "relative", display: "flex", }, variants: { @@ -156,7 +151,7 @@ export const LeftSideBar: React.FC = ({ return (
-
+ {/* Header with Menu, Title, and Toggle button */}
@@ -198,7 +193,7 @@ export const LeftSideBar: React.FC = ({ )} -
+
); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx index fbcdbb78a65..205d5d30658 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx @@ -1,6 +1,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { useCallback, useEffect, useRef, useState } from "react"; +import { GlassPanel } from "../../../../components/glass-panel"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { DifferentialEquationProperties } from "./differential-equation-properties"; @@ -17,17 +18,17 @@ const PANEL_MARGIN = 12; */ export const PropertiesPanel: React.FC = () => { const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, + (state) => state.selectedResourceId ); const globalMode = useEditorStore((state) => state.globalMode); const setPropertiesPanelWidth = useEditorStore( - (state) => state.setPropertiesPanelWidth, + (state) => state.setPropertiesPanelWidth ); const isDiagnosticsPanelOpen = useEditorStore( - (state) => state.isDiagnosticsPanelOpen, + (state) => state.isDiagnosticsPanelOpen ); const diagnosticsPanelHeight = useEditorStore( - (state) => state.diagnosticsPanelHeight, + (state) => state.diagnosticsPanelHeight ); const { @@ -56,7 +57,7 @@ export const PropertiesPanel: React.FC = () => { return newWidth; }); }, - [setPropertiesPanelWidth], + [setPropertiesPanelWidth] ); // Initialize store with starting width @@ -71,7 +72,7 @@ export const PropertiesPanel: React.FC = () => { resizeStartXRef.current = event.clientX; resizeStartWidthRef.current = panelWidth; }, - [panelWidth], + [panelWidth] ); const handleResizeMove = useCallback( @@ -83,11 +84,11 @@ export const PropertiesPanel: React.FC = () => { const deltaX = resizeStartXRef.current - event.clientX; const newWidth = Math.max( 250, - Math.min(800, resizeStartWidthRef.current + deltaX), + Math.min(800, resizeStartWidthRef.current + deltaX) ); setPanelWidth(newWidth); }, - [isResizing, setPanelWidth], + [isResizing, setPanelWidth] ); const handleResizeEnd = useCallback(() => { @@ -131,7 +132,7 @@ export const PropertiesPanel: React.FC = () => { switch (itemType) { case "place": { const placeData = petriNetDefinition.places.find( - (place) => place.id === selectedId, + (place) => place.id === selectedId ); if (placeData) { content = ( @@ -149,7 +150,7 @@ export const PropertiesPanel: React.FC = () => { case "transition": { const transitionData = petriNetDefinition.transitions.find( - (transition) => transition.id === selectedId, + (transition) => transition.id === selectedId ); if (transitionData) { content = ( @@ -168,7 +169,7 @@ export const PropertiesPanel: React.FC = () => { case "type": { const typeData = petriNetDefinition.types.find( - (type) => type.id === selectedId, + (type) => type.id === selectedId ); if (typeData) { content = ( @@ -184,7 +185,7 @@ export const PropertiesPanel: React.FC = () => { case "differentialEquation": { const equationData = petriNetDefinition.differentialEquations.find( - (equation) => equation.id === selectedId, + (equation) => equation.id === selectedId ); if (equationData) { content = ( @@ -202,7 +203,7 @@ export const PropertiesPanel: React.FC = () => { case "parameter": { const parameterData = petriNetDefinition.parameters.find( - (parameter) => parameter.id === selectedId, + (parameter) => parameter.id === selectedId ); if (parameterData) { content = ( @@ -289,46 +290,18 @@ export const PropertiesPanel: React.FC = () => { }} /> -
- {/* Elements with backdrop-filter should not be parent of Monaco Editor, - otherwise Monaco's Hover Widget do not show */} -
- -
- {content} -
-
+ {content} +
); From 93baa8cacb161cb0de46c797a27444f7ee5d8de6 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 00:34:18 +0100 Subject: [PATCH 11/40] Centralize Resizable in GlassPanel --- .../petrinaut/src/components/glass-panel.tsx | 205 +++++++++++++++++- .../components/BottomPanel/bottom-panel.tsx | 71 +----- .../PropertiesPanel/properties-panel.tsx | 130 +++-------- 3 files changed, 238 insertions(+), 168 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx index 4b3c71d0b81..e80f63db851 100644 --- a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -1,5 +1,12 @@ import { css, cx } from "@hashintel/ds-helpers/css"; -import type { CSSProperties, ReactNode } from "react"; +import { + type CSSProperties, + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; const panelContainerStyle = css({ position: "relative", @@ -7,7 +14,6 @@ const panelContainerStyle = css({ backgroundColor: "[rgba(255, 255, 255, 0.7)]", boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", border: "[1px solid rgba(255, 255, 255, 0.8)]", - overflow: "hidden", }); const blurOverlayStyle = css({ @@ -24,6 +30,21 @@ const contentContainerStyle = css({ width: "[100%]", }); +type ResizableEdge = "top" | "bottom" | "left" | "right"; + +interface ResizeConfig { + /** Which edge of the panel is resizable */ + edge: ResizableEdge; + /** Callback when the size changes */ + onResize: (newSize: number) => void; + /** Current size (width for left/right, height for top/bottom) */ + size: number; + /** Minimum size constraint */ + minSize?: number; + /** Maximum size constraint */ + maxSize?: number; +} + interface GlassPanelProps { /** Content to render inside the panel */ children: ReactNode; @@ -37,14 +58,77 @@ interface GlassPanelProps { contentStyle?: CSSProperties; /** Blur amount in pixels (default: 24) */ blur?: number; + /** Configuration for making the panel resizable */ + resizable?: ResizeConfig; } +const RESIZE_HANDLE_SIZE = 9; + +const getResizeHandleStyle = (edge: ResizableEdge): CSSProperties => { + const base: CSSProperties = { + position: "absolute", + background: "transparent", + border: "none", + padding: 0, + zIndex: 1001, + }; + + switch (edge) { + case "top": + return { + ...base, + top: 0, + left: 0, + right: 0, + height: RESIZE_HANDLE_SIZE, + cursor: "ns-resize", + borderRadius: "12px 12px 0 0", + }; + case "bottom": + return { + ...base, + bottom: 0, + left: 0, + right: 0, + height: RESIZE_HANDLE_SIZE, + cursor: "ns-resize", + borderRadius: "0 0 12px 12px", + }; + case "left": + return { + ...base, + top: 0, + left: 0, + bottom: 0, + width: RESIZE_HANDLE_SIZE, + cursor: "ew-resize", + borderRadius: "12px 0 0 12px", + }; + case "right": + return { + ...base, + top: 0, + right: 0, + bottom: 0, + width: RESIZE_HANDLE_SIZE, + cursor: "ew-resize", + borderRadius: "0 12px 12px 0", + }; + } +}; + +const getCursorStyle = (edge: ResizableEdge): string => { + return edge === "top" || edge === "bottom" ? "ns-resize" : "ew-resize"; +}; + /** * GlassPanel provides a frosted glass-like appearance with backdrop blur. * * Uses a separate overlay element for the backdrop-filter to avoid * interfering with child components that use fixed/absolute positioning * (e.g., Monaco Editor hover widgets). + * + * Optionally supports resizing from any edge with the `resizable` prop. */ export const GlassPanel: React.FC = ({ children, @@ -53,9 +137,126 @@ export const GlassPanel: React.FC = ({ contentClassName, contentStyle, blur = 24, + resizable, }) => { + const [isResizing, setIsResizing] = useState(false); + const resizeStartPosRef = useRef(0); + const resizeStartSizeRef = useRef(0); + + const handleResizeStart = useCallback( + (event: React.MouseEvent) => { + if (!resizable) { + return; + } + + event.preventDefault(); + setIsResizing(true); + + const isVertical = + resizable.edge === "top" || resizable.edge === "bottom"; + resizeStartPosRef.current = isVertical ? event.clientY : event.clientX; + resizeStartSizeRef.current = resizable.size; + }, + [resizable] + ); + + const handleResizeMove = useCallback( + (event: MouseEvent) => { + if (!isResizing || !resizable) { + return; + } + + const { edge, onResize, minSize = 100, maxSize = 800 } = resizable; + const isVertical = edge === "top" || edge === "bottom"; + const currentPos = isVertical ? event.clientY : event.clientX; + + // Calculate delta based on edge direction + // For top/left: dragging towards origin increases size + // For bottom/right: dragging away from origin increases size + let delta: number; + if (edge === "top" || edge === "left") { + delta = resizeStartPosRef.current - currentPos; + } else { + delta = currentPos - resizeStartPosRef.current; + } + + const newSize = Math.max( + minSize, + Math.min(maxSize, resizeStartSizeRef.current + delta) + ); + + onResize(newSize); + }, + [isResizing, resizable] + ); + + const handleResizeEnd = useCallback(() => { + setIsResizing(false); + }, []); + + // Handle keyboard resize + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (!resizable) { + return; + } + + const { edge, onResize, size, minSize = 100, maxSize = 800 } = resizable; + const step = 10; + let delta = 0; + + if (edge === "top" || edge === "bottom") { + if (event.key === "ArrowUp") { + delta = edge === "top" ? step : -step; + } else if (event.key === "ArrowDown") { + delta = edge === "top" ? -step : step; + } + } else if (event.key === "ArrowLeft") { + delta = edge === "left" ? step : -step; + } else if (event.key === "ArrowRight") { + delta = edge === "left" ? -step : step; + } + + if (delta !== 0) { + const newSize = Math.max(minSize, Math.min(maxSize, size + delta)); + onResize(newSize); + } + }, + [resizable] + ); + + // Global cursor and event listeners during resize + useEffect(() => { + if (!isResizing || !resizable) { + return; + } + + document.addEventListener("mousemove", handleResizeMove); + document.addEventListener("mouseup", handleResizeEnd); + document.body.style.cursor = getCursorStyle(resizable.edge); + document.body.style.userSelect = "none"; + + return () => { + document.removeEventListener("mousemove", handleResizeMove); + document.removeEventListener("mouseup", handleResizeEnd); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + }, [isResizing, resizable, handleResizeMove, handleResizeEnd]); + return (
+ {/* Resize handle */} + {resizable && ( +
+ {content} +
); }; From fba89eccc577eae9799c538253518eb398462024 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 00:36:01 +0100 Subject: [PATCH 12/40] Update GlassPanel style --- libs/@hashintel/petrinaut/src/components/glass-panel.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx index e80f63db851..ce73b0737fd 100644 --- a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -10,18 +10,18 @@ import { const panelContainerStyle = css({ position: "relative", - borderRadius: "[12px]", + borderRadius: "[7px]", backgroundColor: "[rgba(255, 255, 255, 0.7)]", - boxShadow: "[0 3px 13px rgba(0, 0, 0, 0.1)]", + boxShadow: "[0 2px 11px rgba(0, 0, 0, 0.1)]", border: "[1px solid rgba(255, 255, 255, 0.8)]", }); const blurOverlayStyle = css({ position: "absolute", inset: "[0]", - borderRadius: "[12px]", + borderRadius: "[7px]", pointerEvents: "none", - backdropFilter: "[blur(24px)]", + backdropFilter: "[blur(27px)]", }); const contentContainerStyle = css({ From 73f6dbff9f9e652530b9628ac4ec307a716ef23d Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:14:40 +0100 Subject: [PATCH 13/40] Centralize UI Constants --- libs/@hashintel/petrinaut/src/constants/ui.ts | 21 +++++++++ .../petrinaut/src/state/editor-store.ts | 43 +++++++++++++------ .../components/BottomPanel/bottom-panel.tsx | 21 +++++---- .../components/LeftSideBar/left-sidebar.tsx | 38 +++++++++++++--- .../PropertiesPanel/properties-panel.tsx | 21 +++++---- 5 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/constants/ui.ts diff --git a/libs/@hashintel/petrinaut/src/constants/ui.ts b/libs/@hashintel/petrinaut/src/constants/ui.ts new file mode 100644 index 00000000000..137186f1165 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/constants/ui.ts @@ -0,0 +1,21 @@ +/** + * UI-related constants for the Petrinaut editor. + */ + +// Panel margin (spacing around panels) +export const PANEL_MARGIN = 10; + +// Left Sidebar +export const DEFAULT_LEFT_SIDEBAR_WIDTH = 320; +export const MIN_LEFT_SIDEBAR_WIDTH = 280; +export const MAX_LEFT_SIDEBAR_WIDTH = 500; + +// Properties Panel (right side) +export const DEFAULT_PROPERTIES_PANEL_WIDTH = 450; +export const MIN_PROPERTIES_PANEL_WIDTH = 250; +export const MAX_PROPERTIES_PANEL_WIDTH = 800; + +// Diagnostics/Bottom Panel +export const DEFAULT_DIAGNOSTICS_PANEL_HEIGHT = 180; +export const MIN_DIAGNOSTICS_PANEL_HEIGHT = 100; +export const MAX_DIAGNOSTICS_PANEL_HEIGHT = 600; diff --git a/libs/@hashintel/petrinaut/src/state/editor-store.ts b/libs/@hashintel/petrinaut/src/state/editor-store.ts index 5f436d25347..6155f753f6c 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-store.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-store.ts @@ -1,6 +1,12 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; +import { + DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, + DEFAULT_LEFT_SIDEBAR_WIDTH, + DEFAULT_PROPERTIES_PANEL_WIDTH, +} from "../constants/ui"; + export type DraggingStateByNodeId = Record< string, { dragging: boolean; position: { x: number; y: number } } @@ -19,6 +25,8 @@ export type EditorState = { // UI state isLeftSidebarOpen: boolean; setLeftSidebarOpen: (isOpen: boolean) => void; + leftSidebarWidth: number; + setLeftSidebarWidth: (width: number) => void; // Properties panel width (for DiagnosticsPanel positioning) propertiesPanelWidth: number; @@ -46,7 +54,7 @@ export type EditorState = { draggingStateByNodeId: DraggingStateByNodeId; setDraggingStateByNodeId: (state: DraggingStateByNodeId) => void; updateDraggingStateByNodeId: ( - updater: (state: DraggingStateByNodeId) => DraggingStateByNodeId, + updater: (state: DraggingStateByNodeId) => DraggingStateByNodeId ) => void; resetDraggingState: () => void; @@ -77,9 +85,15 @@ export function createEditorStore() { type: "setLeftSidebarOpen", isOpen, }), + leftSidebarWidth: DEFAULT_LEFT_SIDEBAR_WIDTH, + setLeftSidebarWidth: (width) => + set({ leftSidebarWidth: width }, false, { + type: "setLeftSidebarWidth", + width, + }), // Properties panel width - propertiesPanelWidth: 450, + propertiesPanelWidth: DEFAULT_PROPERTIES_PANEL_WIDTH, setPropertiesPanelWidth: (width) => set({ propertiesPanelWidth: width }, false, { type: "setPropertiesPanelWidth", @@ -99,9 +113,9 @@ export function createEditorStore() { isDiagnosticsPanelOpen: !state.isDiagnosticsPanelOpen, }), false, - "toggleDiagnosticsPanel", + "toggleDiagnosticsPanel" ), - diagnosticsPanelHeight: 180, + diagnosticsPanelHeight: DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, setDiagnosticsPanelHeight: (height) => set({ diagnosticsPanelHeight: height }, false, { type: "setDiagnosticsPanelHeight", @@ -131,7 +145,7 @@ export function createEditorStore() { return { selectedItemIds: newSet }; }, false, - { type: "addSelectedItemId", id }, + { type: "addSelectedItemId", id } ), removeSelectedItemId: (id) => set( @@ -141,7 +155,7 @@ export function createEditorStore() { return { selectedItemIds: newSet }; }, false, - { type: "removeSelectedItemId", id }, + { type: "removeSelectedItemId", id } ), clearSelection: () => set({ selectedItemIds: new Set() }, false, "clearSelection"), @@ -152,7 +166,7 @@ export function createEditorStore() { set( { draggingStateByNodeId: state }, false, - "setDraggingStateByNodeId", + "setDraggingStateByNodeId" ), updateDraggingStateByNodeId: (updater) => set( @@ -160,7 +174,7 @@ export function createEditorStore() { draggingStateByNodeId: updater(state.draggingStateByNodeId), }), false, - "updateDraggingStateByNodeId", + "updateDraggingStateByNodeId" ), resetDraggingState: () => set({ draggingStateByNodeId: {} }, false, "resetDraggingState"), @@ -171,20 +185,21 @@ export function createEditorStore() { globalMode: "edit", editionMode: "select", isLeftSidebarOpen: true, - propertiesPanelWidth: 450, + leftSidebarWidth: DEFAULT_LEFT_SIDEBAR_WIDTH, + propertiesPanelWidth: DEFAULT_PROPERTIES_PANEL_WIDTH, isDiagnosticsPanelOpen: false, - diagnosticsPanelHeight: 180, + diagnosticsPanelHeight: DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, selectedResourceId: null, selectedItemIds: new Set(), draggingStateByNodeId: {}, }, false, - { type: "initializeEditorStore" }, + { type: "initializeEditorStore" } ); }, // for some reason 'create' doesn't raise an error if a function in the type is missing - }) satisfies EditorState, - { name: "Editor Store" }, - ), + } satisfies EditorState), + { name: "Editor Store" } + ) ); } diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 19154a18bf0..95dc6b89d16 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -2,17 +2,16 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { useState } from "react"; import { GlassPanel } from "../../../../components/glass-panel"; +import { + MAX_DIAGNOSTICS_PANEL_HEIGHT, + MIN_DIAGNOSTICS_PANEL_HEIGHT, + PANEL_MARGIN, +} from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; import { DiagnosticsContent } from "./diagnostics-content"; import { ParametersContent } from "./parameters-content"; import { SimulationSettingsContent } from "./simulation-settings-content"; -// Position offsets (accounting for sidebar padding/margins) -const MIN_HEIGHT = 100; -const MAX_HEIGHT = 600; -const LEFT_SIDEBAR_WIDTH = 344; // 320px + 24px padding -const PANEL_MARGIN = 12; - type BottomPanelTab = "diagnostics" | "simulation-settings" | "parameters"; const panelContainerStyle = css({ @@ -79,6 +78,7 @@ const tabs: { id: BottomPanelTab; label: string }[] = [ export const BottomPanel: React.FC = () => { const isOpen = useEditorStore((state) => state.isDiagnosticsPanelOpen); const isLeftSidebarOpen = useEditorStore((state) => state.isLeftSidebarOpen); + const leftSidebarWidth = useEditorStore((state) => state.leftSidebarWidth); const panelHeight = useEditorStore((state) => state.diagnosticsPanelHeight); const setDiagnosticsPanelHeight = useEditorStore( (state) => state.setDiagnosticsPanelHeight @@ -91,7 +91,10 @@ export const BottomPanel: React.FC = () => { } // Calculate left position based on left sidebar state - const leftOffset = isLeftSidebarOpen ? LEFT_SIDEBAR_WIDTH : PANEL_MARGIN; + // Add sidebar padding (12px each side) when sidebar is open + const leftOffset = isLeftSidebarOpen + ? leftSidebarWidth + PANEL_MARGIN * 2 + : PANEL_MARGIN; function renderContent() { switch (activeTab) { @@ -120,8 +123,8 @@ export const BottomPanel: React.FC = () => { edge: "top", size: panelHeight, onResize: setDiagnosticsPanelHeight, - minSize: MIN_HEIGHT, - maxSize: MAX_HEIGHT, + minSize: MIN_DIAGNOSTICS_PANEL_HEIGHT, + maxSize: MAX_DIAGNOSTICS_PANEL_HEIGHT, }} > {/* Tab Header */} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx index 9ee2b64f9b9..dab5bc788c6 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx @@ -6,6 +6,11 @@ import { import { GlassPanel } from "../../../../components/glass-panel"; import type { MenuItem } from "../../../../components/menu"; +import { + MAX_LEFT_SIDEBAR_WIDTH, + MIN_LEFT_SIDEBAR_WIDTH, + PANEL_MARGIN, +} from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; import { DifferentialEquationsSection } from "./differential-equations-section"; import { FloatingTitle } from "./floating-title"; @@ -16,7 +21,6 @@ import { TypesSection } from "./types-section"; const outerContainerStyle = cva({ base: { position: "fixed", - padding: "[12px]", zIndex: 1000, display: "flex", }, @@ -29,8 +33,6 @@ const outerContainerStyle = cva({ height: "[100%]", }, false: { - top: "[12px]", - left: "[12px]", bottom: "[auto]", height: "[auto]", }, @@ -46,7 +48,6 @@ const panelContentStyle = cva({ isOpen: { true: { height: "[100%]", - width: "[320px]", padding: "[16px]", flexDirection: "column", gap: "[16px]", @@ -137,6 +138,7 @@ interface LeftSideBarProps { * LeftSideBar displays the menu, title, and tools. * When collapsed: shows a horizontal bar with menu, title, and toggle button. * When open: shows the full sidebar with tools and content. + * Resizable from the right edge when open. */ export const LeftSideBar: React.FC = ({ hideNetManagementControls, @@ -148,10 +150,34 @@ export const LeftSideBar: React.FC = ({ const setLeftSidebarOpen = useEditorStore( (state) => state.setLeftSidebarOpen ); + const leftSidebarWidth = useEditorStore((state) => state.leftSidebarWidth); + const setLeftSidebarWidth = useEditorStore( + (state) => state.setLeftSidebarWidth + ); return ( -
- +
+ {/* Header with Menu, Title, and Toggle button */}
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx index b1a6f442679..65b9f612eb7 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx @@ -2,6 +2,12 @@ import { css } from "@hashintel/ds-helpers/css"; import { useCallback, useEffect, useState } from "react"; import { GlassPanel } from "../../../../components/glass-panel"; +import { + DEFAULT_PROPERTIES_PANEL_WIDTH, + MAX_PROPERTIES_PANEL_WIDTH, + MIN_PROPERTIES_PANEL_WIDTH, + PANEL_MARGIN, +} from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { DifferentialEquationProperties } from "./differential-equation-properties"; @@ -10,11 +16,6 @@ import { PlaceProperties } from "./place-properties"; import { TransitionProperties } from "./transition-properties"; import { TypeProperties } from "./type-properties"; -const startingWidth = 450; -const MIN_WIDTH = 250; -const MAX_WIDTH = 800; -const PANEL_MARGIN = 12; - /** * PropertiesPanel displays properties and controls for the selected node/edge. */ @@ -44,7 +45,9 @@ export const PropertiesPanel: React.FC = () => { updateParameter, } = useSDCPNContext(); - const [panelWidth, setPanelWidthLocal] = useState(startingWidth); + const [panelWidth, setPanelWidthLocal] = useState( + DEFAULT_PROPERTIES_PANEL_WIDTH + ); // Sync panel width with global store const handleResize = useCallback( @@ -57,7 +60,7 @@ export const PropertiesPanel: React.FC = () => { // Initialize store with starting width useEffect(() => { - setPropertiesPanelWidth(startingWidth); + setPropertiesPanelWidth(DEFAULT_PROPERTIES_PANEL_WIDTH); }, [setPropertiesPanelWidth]); // Don't show panel if nothing is selected @@ -217,8 +220,8 @@ export const PropertiesPanel: React.FC = () => { edge: "left", size: panelWidth, onResize: handleResize, - minSize: MIN_WIDTH, - maxSize: MAX_WIDTH, + minSize: MIN_PROPERTIES_PANEL_WIDTH, + maxSize: MAX_PROPERTIES_PANEL_WIDTH, }} > {content} From bf5f347ff4f4b9b7518d92de173617838ac0d3d5 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:22:38 +0100 Subject: [PATCH 14/40] Better resizer handle --- .../petrinaut/src/components/glass-panel.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx index ce73b0737fd..f6e29688e75 100644 --- a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -62,7 +62,8 @@ interface GlassPanelProps { resizable?: ResizeConfig; } -const RESIZE_HANDLE_SIZE = 9; +const RESIZE_HANDLE_SIZE = 20; +const RESIZE_HANDLE_OFFSET = -Math.floor(RESIZE_HANDLE_SIZE / 2); const getResizeHandleStyle = (edge: ResizableEdge): CSSProperties => { const base: CSSProperties = { @@ -77,42 +78,38 @@ const getResizeHandleStyle = (edge: ResizableEdge): CSSProperties => { case "top": return { ...base, - top: 0, + top: RESIZE_HANDLE_OFFSET, left: 0, right: 0, height: RESIZE_HANDLE_SIZE, cursor: "ns-resize", - borderRadius: "12px 12px 0 0", }; case "bottom": return { ...base, - bottom: 0, + bottom: RESIZE_HANDLE_OFFSET, left: 0, right: 0, height: RESIZE_HANDLE_SIZE, cursor: "ns-resize", - borderRadius: "0 0 12px 12px", }; case "left": return { ...base, top: 0, - left: 0, + left: RESIZE_HANDLE_OFFSET, bottom: 0, width: RESIZE_HANDLE_SIZE, cursor: "ew-resize", - borderRadius: "12px 0 0 12px", }; case "right": return { ...base, top: 0, - right: 0, + right: RESIZE_HANDLE_OFFSET, bottom: 0, width: RESIZE_HANDLE_SIZE, cursor: "ew-resize", - borderRadius: "0 12px 12px 0", }; } }; From 079fa5be5d5577ae982dbd3ecd3d6d82b7f7e287 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:24:54 +0100 Subject: [PATCH 15/40] Move RESIZE_HANDLE constants in UI constants --- libs/@hashintel/petrinaut/src/components/glass-panel.tsx | 5 ++--- libs/@hashintel/petrinaut/src/constants/ui.ts | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx index f6e29688e75..d28a8740b98 100644 --- a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -8,6 +8,8 @@ import { useState, } from "react"; +import { RESIZE_HANDLE_OFFSET, RESIZE_HANDLE_SIZE } from "../constants/ui"; + const panelContainerStyle = css({ position: "relative", borderRadius: "[7px]", @@ -62,9 +64,6 @@ interface GlassPanelProps { resizable?: ResizeConfig; } -const RESIZE_HANDLE_SIZE = 20; -const RESIZE_HANDLE_OFFSET = -Math.floor(RESIZE_HANDLE_SIZE / 2); - const getResizeHandleStyle = (edge: ResizableEdge): CSSProperties => { const base: CSSProperties = { position: "absolute", diff --git a/libs/@hashintel/petrinaut/src/constants/ui.ts b/libs/@hashintel/petrinaut/src/constants/ui.ts index 137186f1165..3bcb846c2f7 100644 --- a/libs/@hashintel/petrinaut/src/constants/ui.ts +++ b/libs/@hashintel/petrinaut/src/constants/ui.ts @@ -5,6 +5,10 @@ // Panel margin (spacing around panels) export const PANEL_MARGIN = 10; +// Resize handle +export const RESIZE_HANDLE_SIZE = 20; +export const RESIZE_HANDLE_OFFSET = -Math.floor(RESIZE_HANDLE_SIZE / 2); + // Left Sidebar export const DEFAULT_LEFT_SIDEBAR_WIDTH = 320; export const MIN_LEFT_SIDEBAR_WIDTH = 280; From 296b2d8839c633fa3d9a9b2132908eb001c44855 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:32:26 +0100 Subject: [PATCH 16/40] Make tabs a bit cleaner --- .../views/Editor/components/BottomPanel/bottom-panel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 95dc6b89d16..8f9c01bc7c2 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -24,7 +24,7 @@ const headerStyle = css({ alignItems: "center", gap: "[4px]", borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", - padding: "[4px 8px]", + padding: "[2px 2px 6px 2px]", flexShrink: 0, }); @@ -32,8 +32,8 @@ const tabButtonStyle = cva({ base: { fontSize: "[12px]", fontWeight: "[500]", - padding: "[6px 12px]", - borderRadius: "[6px]", + padding: "[4px 12px]", + borderRadius: "[3px]", border: "none", cursor: "pointer", transition: "[all 0.15s ease]", From 160da9a468394c910ea023f5a80c69cf71e45d63 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:39:16 +0100 Subject: [PATCH 17/40] Move activeBottomPanel to EditorStore --- .../petrinaut/src/state/editor-store.ts | 15 ++++++++++++++- .../components/BottomPanel/bottom-panel.tsx | 8 +++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/state/editor-store.ts b/libs/@hashintel/petrinaut/src/state/editor-store.ts index 6155f753f6c..d5077100a9c 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-store.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-store.ts @@ -14,6 +14,10 @@ export type DraggingStateByNodeId = Record< type EditorGlobalMode = "edit" | "simulate"; type EditorEditionMode = "select" | "pan" | "add-place" | "add-transition"; +export type BottomPanelTab = + | "diagnostics" + | "simulation-settings" + | "parameters"; export type EditorState = { globalMode: EditorGlobalMode; @@ -32,12 +36,14 @@ export type EditorState = { propertiesPanelWidth: number; setPropertiesPanelWidth: (width: number) => void; - // Diagnostics panel visibility and height + // Diagnostics/Bottom panel visibility, height, and active tab isDiagnosticsPanelOpen: boolean; setDiagnosticsPanelOpen: (isOpen: boolean) => void; toggleDiagnosticsPanel: () => void; diagnosticsPanelHeight: number; setDiagnosticsPanelHeight: (height: number) => void; + activeBottomPanelTab: BottomPanelTab; + setActiveBottomPanelTab: (tab: BottomPanelTab) => void; // Selected Resource ID (for properties panel) selectedResourceId: string | null; @@ -121,6 +127,12 @@ export function createEditorStore() { type: "setDiagnosticsPanelHeight", height, }), + activeBottomPanelTab: "diagnostics", + setActiveBottomPanelTab: (tab) => + set({ activeBottomPanelTab: tab }, false, { + type: "setActiveBottomPanelTab", + tab, + }), // Selected Resource ID selectedResourceId: null, @@ -189,6 +201,7 @@ export function createEditorStore() { propertiesPanelWidth: DEFAULT_PROPERTIES_PANEL_WIDTH, isDiagnosticsPanelOpen: false, diagnosticsPanelHeight: DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, + activeBottomPanelTab: "diagnostics", selectedResourceId: null, selectedItemIds: new Set(), draggingStateByNodeId: {}, diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 8f9c01bc7c2..573e626bafd 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -1,5 +1,4 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; import { GlassPanel } from "../../../../components/glass-panel"; import { @@ -8,12 +7,11 @@ import { PANEL_MARGIN, } from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; +import type { BottomPanelTab } from "../../../../state/editor-store"; import { DiagnosticsContent } from "./diagnostics-content"; import { ParametersContent } from "./parameters-content"; import { SimulationSettingsContent } from "./simulation-settings-content"; -type BottomPanelTab = "diagnostics" | "simulation-settings" | "parameters"; - const panelContainerStyle = css({ display: "flex", flexDirection: "column", @@ -83,8 +81,8 @@ export const BottomPanel: React.FC = () => { const setDiagnosticsPanelHeight = useEditorStore( (state) => state.setDiagnosticsPanelHeight ); - - const [activeTab, setActiveTab] = useState("diagnostics"); + const activeTab = useEditorStore((state) => state.activeBottomPanelTab); + const setActiveTab = useEditorStore((state) => state.setActiveBottomPanelTab); if (!isOpen) { return null; From 7559909366816f1e7326a66839a3c0ac5c6ddedd Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:43:54 +0100 Subject: [PATCH 18/40] Rename DiagnosticsPanel to BottomPanel --- libs/@hashintel/petrinaut/src/constants/ui.ts | 8 ++-- .../petrinaut/src/state/editor-store.ts | 44 +++++++++---------- .../components/BottomBar/bottom-bar.tsx | 22 ++++------ .../components/BottomPanel/bottom-panel.tsx | 18 ++++---- .../PropertiesPanel/properties-panel.tsx | 16 +++---- 5 files changed, 49 insertions(+), 59 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/constants/ui.ts b/libs/@hashintel/petrinaut/src/constants/ui.ts index 3bcb846c2f7..ddcc7960c50 100644 --- a/libs/@hashintel/petrinaut/src/constants/ui.ts +++ b/libs/@hashintel/petrinaut/src/constants/ui.ts @@ -19,7 +19,7 @@ export const DEFAULT_PROPERTIES_PANEL_WIDTH = 450; export const MIN_PROPERTIES_PANEL_WIDTH = 250; export const MAX_PROPERTIES_PANEL_WIDTH = 800; -// Diagnostics/Bottom Panel -export const DEFAULT_DIAGNOSTICS_PANEL_HEIGHT = 180; -export const MIN_DIAGNOSTICS_PANEL_HEIGHT = 100; -export const MAX_DIAGNOSTICS_PANEL_HEIGHT = 600; +// Bottom Panel +export const DEFAULT_BOTTOM_PANEL_HEIGHT = 180; +export const MIN_BOTTOM_PANEL_HEIGHT = 100; +export const MAX_BOTTOM_PANEL_HEIGHT = 600; diff --git a/libs/@hashintel/petrinaut/src/state/editor-store.ts b/libs/@hashintel/petrinaut/src/state/editor-store.ts index d5077100a9c..55a6f47afdf 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-store.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { - DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, + DEFAULT_BOTTOM_PANEL_HEIGHT, DEFAULT_LEFT_SIDEBAR_WIDTH, DEFAULT_PROPERTIES_PANEL_WIDTH, } from "../constants/ui"; @@ -32,16 +32,16 @@ export type EditorState = { leftSidebarWidth: number; setLeftSidebarWidth: (width: number) => void; - // Properties panel width (for DiagnosticsPanel positioning) + // Properties panel width (for BottomPanel positioning) propertiesPanelWidth: number; setPropertiesPanelWidth: (width: number) => void; - // Diagnostics/Bottom panel visibility, height, and active tab - isDiagnosticsPanelOpen: boolean; - setDiagnosticsPanelOpen: (isOpen: boolean) => void; - toggleDiagnosticsPanel: () => void; - diagnosticsPanelHeight: number; - setDiagnosticsPanelHeight: (height: number) => void; + // Bottom panel visibility, height, and active tab + isBottomPanelOpen: boolean; + setBottomPanelOpen: (isOpen: boolean) => void; + toggleBottomPanel: () => void; + bottomPanelHeight: number; + setBottomPanelHeight: (height: number) => void; activeBottomPanelTab: BottomPanelTab; setActiveBottomPanelTab: (tab: BottomPanelTab) => void; @@ -106,25 +106,25 @@ export function createEditorStore() { width, }), - // Diagnostics panel visibility and height - isDiagnosticsPanelOpen: false, - setDiagnosticsPanelOpen: (isOpen) => - set({ isDiagnosticsPanelOpen: isOpen }, false, { - type: "setDiagnosticsPanelOpen", + // Bottom panel visibility and height + isBottomPanelOpen: false, + setBottomPanelOpen: (isOpen) => + set({ isBottomPanelOpen: isOpen }, false, { + type: "setBottomPanelOpen", isOpen, }), - toggleDiagnosticsPanel: () => + toggleBottomPanel: () => set( (state) => ({ - isDiagnosticsPanelOpen: !state.isDiagnosticsPanelOpen, + isBottomPanelOpen: !state.isBottomPanelOpen, }), false, - "toggleDiagnosticsPanel" + "toggleBottomPanel" ), - diagnosticsPanelHeight: DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, - setDiagnosticsPanelHeight: (height) => - set({ diagnosticsPanelHeight: height }, false, { - type: "setDiagnosticsPanelHeight", + bottomPanelHeight: DEFAULT_BOTTOM_PANEL_HEIGHT, + setBottomPanelHeight: (height) => + set({ bottomPanelHeight: height }, false, { + type: "setBottomPanelHeight", height, }), activeBottomPanelTab: "diagnostics", @@ -199,8 +199,8 @@ export function createEditorStore() { isLeftSidebarOpen: true, leftSidebarWidth: DEFAULT_LEFT_SIDEBAR_WIDTH, propertiesPanelWidth: DEFAULT_PROPERTIES_PANEL_WIDTH, - isDiagnosticsPanelOpen: false, - diagnosticsPanelHeight: DEFAULT_DIAGNOSTICS_PANEL_HEIGHT, + isBottomPanelOpen: false, + bottomPanelHeight: DEFAULT_BOTTOM_PANEL_HEIGHT, activeBottomPanelTab: "diagnostics", selectedResourceId: null, selectedItemIds: new Set(), diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index 031817b24aa..224a22f88f9 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -51,15 +51,9 @@ export const BottomBar: React.FC = ({ editionMode, onEditionModeChange, }) => { - const isDiagnosticsPanelOpen = useEditorStore( - (state) => state.isDiagnosticsPanelOpen, - ); - const toggleDiagnosticsPanel = useEditorStore( - (state) => state.toggleDiagnosticsPanel, - ); - const diagnosticsPanelHeight = useEditorStore( - (state) => state.diagnosticsPanelHeight, - ); + const isBottomPanelOpen = useEditorStore((state) => state.isBottomPanelOpen); + const toggleBottomPanel = useEditorStore((state) => state.toggleBottomPanel); + const bottomPanelHeight = useEditorStore((state) => state.bottomPanelHeight); const { totalDiagnosticsCount } = useCheckerContext(); const hasDiagnostics = totalDiagnosticsCount > 0; @@ -77,9 +71,9 @@ export const BottomBar: React.FC = ({ // Setup keyboard shortcuts useKeyboardShortcuts(mode, onEditionModeChange); - // Calculate bottom offset based on diagnostics panel visibility - const bottomOffset = isDiagnosticsPanelOpen - ? diagnosticsPanelHeight + 12 + 24 // panel height + margin + spacing + // Calculate bottom offset based on bottom panel visibility + const bottomOffset = isBottomPanelOpen + ? bottomPanelHeight + 12 + 24 // panel height + margin + spacing : 24; return ( @@ -95,8 +89,8 @@ export const BottomBar: React.FC = ({ >
{ - const isOpen = useEditorStore((state) => state.isDiagnosticsPanelOpen); + const isOpen = useEditorStore((state) => state.isBottomPanelOpen); const isLeftSidebarOpen = useEditorStore((state) => state.isLeftSidebarOpen); const leftSidebarWidth = useEditorStore((state) => state.leftSidebarWidth); - const panelHeight = useEditorStore((state) => state.diagnosticsPanelHeight); - const setDiagnosticsPanelHeight = useEditorStore( - (state) => state.setDiagnosticsPanelHeight + const panelHeight = useEditorStore((state) => state.bottomPanelHeight); + const setBottomPanelHeight = useEditorStore( + (state) => state.setBottomPanelHeight ); const activeTab = useEditorStore((state) => state.activeBottomPanelTab); const setActiveTab = useEditorStore((state) => state.setActiveBottomPanelTab); @@ -120,9 +120,9 @@ export const BottomPanel: React.FC = () => { resizable={{ edge: "top", size: panelHeight, - onResize: setDiagnosticsPanelHeight, - minSize: MIN_DIAGNOSTICS_PANEL_HEIGHT, - maxSize: MAX_DIAGNOSTICS_PANEL_HEIGHT, + onResize: setBottomPanelHeight, + minSize: MIN_BOTTOM_PANEL_HEIGHT, + maxSize: MAX_BOTTOM_PANEL_HEIGHT, }} > {/* Tab Header */} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx index 65b9f612eb7..41b0b3b79ad 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx @@ -27,12 +27,8 @@ export const PropertiesPanel: React.FC = () => { const setPropertiesPanelWidth = useEditorStore( (state) => state.setPropertiesPanelWidth ); - const isDiagnosticsPanelOpen = useEditorStore( - (state) => state.isDiagnosticsPanelOpen - ); - const diagnosticsPanelHeight = useEditorStore( - (state) => state.diagnosticsPanelHeight - ); + const isBottomPanelOpen = useEditorStore((state) => state.isBottomPanelOpen); + const bottomPanelHeight = useEditorStore((state) => state.bottomPanelHeight); const { getItemType, @@ -185,10 +181,10 @@ export const PropertiesPanel: React.FC = () => { ); } - // Calculate bottom offset based on diagnostics panel visibility - // Gap between PropertiesPanel and DiagnosticsPanel matches gap between LeftSideBar and DiagnosticsPanel - const bottomOffset = isDiagnosticsPanelOpen - ? diagnosticsPanelHeight + PANEL_MARGIN + // Calculate bottom offset based on bottom panel visibility + // Gap between PropertiesPanel and BottomPanel matches gap between LeftSideBar and BottomPanel + const bottomOffset = isBottomPanelOpen + ? bottomPanelHeight + PANEL_MARGIN : 0; return ( From ee733899e114df81b229c9b703a2955fb324e4b8 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 01:59:33 +0100 Subject: [PATCH 19/40] Use Play/Pause/Stop icons --- .../@hashintel/petrinaut/src/feature-flags.ts | 1 - .../components/BottomBar/bottom-bar.tsx | 2 +- .../BottomBar/simulation-controls.tsx | 51 +++++++------------ 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/feature-flags.ts b/libs/@hashintel/petrinaut/src/feature-flags.ts index afd4dae29ad..5d4bcdf431d 100644 --- a/libs/@hashintel/petrinaut/src/feature-flags.ts +++ b/libs/@hashintel/petrinaut/src/feature-flags.ts @@ -1,4 +1,3 @@ export const FEATURE_FLAGS = { - RUNNING_MAN_ICON: true, REORDER_TRANSITION_ARCS: false, }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index 224a22f88f9..1d56fe01f42 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -26,7 +26,7 @@ const toolbarContainerStyle = css({ const dividerStyle = css({ background: "core.gray.20", width: "[1px]", - height: "[40px]", + height: "[16px]", margin: "[0 4px]", }); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 09facfe7fda..1074eff35b6 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -1,9 +1,7 @@ import { css } from "@hashintel/ds-helpers/css"; -import { BiRun } from "react-icons/bi"; -import { TbCircle, TbRefresh, TbSquare } from "react-icons/tb"; +import { IoMdPause, IoMdPlay, IoMdSquare } from "react-icons/io"; import { Tooltip } from "../../../../components/tooltip"; -import { FEATURE_FLAGS } from "../../../../feature-flags"; import { useSimulationStore } from "../../../../state/simulation-provider"; const containerStyle = css({ @@ -11,6 +9,7 @@ const containerStyle = css({ alignItems: "center", padding: "[0 12px]", gap: "[12px]", + fontSize: "[24px]", }); const playPauseButtonStyle = css({ @@ -18,16 +17,9 @@ const playPauseButtonStyle = css({ display: "flex", alignItems: "center", justifyContent: "center", - width: "[32px]", - height: "[32px]", - borderRadius: "[50%]", border: "none", - background: "core.red.50", - color: "[white]", - fontSize: "[20px]", transition: "[all 0.2s ease]", "&:hover:not(:disabled)": { - background: "core.red.60", transform: "[scale(1.05)]", }, "&:disabled": { @@ -93,7 +85,6 @@ const resetButtonStyle = css({ border: "none", background: "[transparent]", color: "core.gray.80", - fontSize: "[18px]", transition: "[all 0.2s ease]", "&:hover:not(:disabled)": { background: "core.gray.10", @@ -184,16 +175,25 @@ export const SimulationControls: React.FC = ({ : "Continue simulation" } > - {isRunning ? ( - - ) : FEATURE_FLAGS.RUNNING_MAN_ICON ? ( - - ) : ( - - )} + {isRunning ? : } + {/* Reset button - only visible when simulation exists */} + {hasSimulation && ( + + + + )} + {/* Frame controls - only visible when simulation exists */} {hasSimulation && ( <> @@ -217,21 +217,6 @@ export const SimulationControls: React.FC = ({ /> )} - - {/* Reset button - only visible when simulation exists */} - {hasSimulation && ( - - - - )}
); }; From 3465b28d37e93b2df18e63383c0de158a2d07761 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 02:06:03 +0100 Subject: [PATCH 20/40] Open Diagnostics when clicking on Run Simulation when errors disable it --- .../BottomBar/simulation-controls.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 1074eff35b6..7df01e342fb 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -2,6 +2,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { IoMdPause, IoMdPlay, IoMdSquare } from "react-icons/io"; import { Tooltip } from "../../../../components/tooltip"; +import { useEditorStore } from "../../../../state/editor-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; const containerStyle = css({ @@ -19,12 +20,11 @@ const playPauseButtonStyle = css({ justifyContent: "center", border: "none", transition: "[all 0.2s ease]", - "&:hover:not(:disabled)": { + "&:hover:not([data-disabled])": { transform: "[scale(1.05)]", }, - "&:disabled": { + "&[data-disabled]": { opacity: "[0.5]", - cursor: "not-allowed", }, }); @@ -117,14 +117,32 @@ export const SimulationControls: React.FC = ({ (state) => state.setCurrentlyViewedFrame ); + const setBottomPanelOpen = useEditorStore( + (state) => state.setBottomPanelOpen + ); + const setActiveBottomPanelTab = useEditorStore( + (state) => state.setActiveBottomPanelTab + ); + const isDisabled = disabled; + function openDiagnosticsPanel() { + setActiveBottomPanelTab("diagnostics"); + setBottomPanelOpen(true); + } + const totalFrames = simulation?.frames.length ?? 0; const hasSimulation = simulation !== null; const isRunning = simulationState === "Running"; const elapsedTime = simulation ? currentlyViewedFrame * simulation.dt : 0; const handlePlayPause = () => { + // If disabled due to errors, open diagnostics panel instead + if (isDisabled) { + openDiagnosticsPanel(); + return; + } + if (simulationState === "NotRun") { // Initialize and start continuous simulation initialize({ @@ -165,10 +183,12 @@ export const SimulationControls: React.FC = ({
); }; - - From 3e0f7e058cc763a7de1203d4b7dfdaf74ba941ac Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 02:33:29 +0100 Subject: [PATCH 22/40] Cleaner BottomPanel --- .../views/Editor/components/BottomPanel/bottom-panel.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index e0df89eb2bb..4eeea468313 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -21,8 +21,7 @@ const headerStyle = css({ display: "flex", alignItems: "center", gap: "[4px]", - borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", - padding: "[2px 2px 6px 2px]", + padding: "[2px]", flexShrink: 0, }); @@ -30,7 +29,7 @@ const tabButtonStyle = cva({ base: { fontSize: "[12px]", fontWeight: "[500]", - padding: "[4px 12px]", + padding: "[4px 10px]", borderRadius: "[3px]", border: "none", cursor: "pointer", @@ -56,7 +55,7 @@ const tabButtonStyle = cva({ const contentStyle = css({ fontSize: "[12px]", - padding: "[8px 16px]", + padding: "[12px 10px]", flex: "[1]", overflowY: "auto", }); From 365087c87a211768a47b6b24af3ed98c1bdcda9e Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 02:34:01 +0100 Subject: [PATCH 23/40] Remove Simulation State from Simulation Settings --- .../simulation-settings-content.tsx | 63 +------------------ 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx index 7af189d88e5..c110dae3678 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -118,41 +118,10 @@ const editButtonStyle = css({ alignSelf: "flex-start", }); -const resetButtonStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - background: "[transparent]", - border: "1px solid", - borderColor: "core.gray.30", - borderRadius: "radius.4", - cursor: "pointer", - color: "core.gray.70", +const editButtonIconStyle = css({ fontSize: "[12px]", - fontWeight: "[500]", - padding: "[4px 12px]", - alignSelf: "flex-start", - _hover: { - backgroundColor: "core.gray.10", - borderColor: "core.gray.40", - }, }); -const getStateColor = (state: string): string => { - switch (state) { - case "Running": - return "#1976d2"; - case "Complete": - return "#2e7d32"; - case "Error": - return "#d32f2f"; - case "Paused": - return "#ed6c02"; - default: - return "rgba(0, 0, 0, 0.7)"; - } -}; - /** * SimulationSettingsContent displays simulation settings in the BottomPanel. */ @@ -161,7 +130,6 @@ export const SimulationSettingsContent: React.FC = () => { const simulationState = useSimulationStore((state) => state.state); const simulationError = useSimulationStore((state) => state.error); const errorItemId = useSimulationStore((state) => state.errorItemId); - const reset = useSimulationStore((state) => state.reset); const dt = useSimulationStore((state) => state.dt); const setDt = useSimulationStore((state) => state.setDt); const setSelectedResourceId = useEditorStore( @@ -200,7 +168,6 @@ export const SimulationSettingsContent: React.FC = () => { className={isSimulationActive ? inputDisabledStyle : inputStyle} />
- {/* ODE Solver Method Select */}
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- it can't tell it's the same ID */} @@ -217,32 +184,6 @@ export const SimulationSettingsContent: React.FC = () => {
- - {/* Simulation State */} -
- Simulation State - - {simulationState === "NotRun" ? "Not Started" : simulationState} - -
- - {/* Reset Button */} - {simulationState !== "NotRun" && ( - - )}
{/* Error Display */} @@ -259,7 +200,7 @@ export const SimulationSettingsContent: React.FC = () => { className={editButtonStyle} > Edit Item - + )}
From 8b4cbab26ca1a019f6c70c3690ad461cdc714566 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 10:36:07 +0100 Subject: [PATCH 24/40] BottomPanel styles --- .../components/BottomPanel/bottom-panel.tsx | 10 +- .../simulation-settings-content.tsx | 243 +++++++++++++----- 2 files changed, 186 insertions(+), 67 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 4eeea468313..2b01f1b06ca 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -27,24 +27,28 @@ const headerStyle = css({ const tabButtonStyle = cva({ base: { - fontSize: "[12px]", + fontSize: "[11px]", fontWeight: "[500]", padding: "[4px 10px]", + textTransform: "uppercase", borderRadius: "[3px]", border: "none", cursor: "pointer", - transition: "[all 0.15s ease]", + transition: "[all 0.3s ease]", background: "[transparent]", }, variants: { active: { true: { + opacity: "[1]", backgroundColor: "[rgba(0, 0, 0, 0.08)]", color: "core.gray.90", }, false: { + opacity: "[0.6]", color: "core.gray.60", _hover: { + opacity: "[1]", backgroundColor: "[rgba(0, 0, 0, 0.04)]", color: "core.gray.80", }, @@ -55,7 +59,7 @@ const tabButtonStyle = cva({ const contentStyle = css({ fontSize: "[12px]", - padding: "[12px 10px]", + padding: "[12px 12px]", flex: "[1]", overflowY: "auto", }); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx index c110dae3678..120b39e854a 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -3,15 +3,32 @@ import { useState } from "react"; import { TbArrowRight } from "react-icons/tb"; import { useEditorStore } from "../../../../state/editor-provider"; +import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; +const containerStyle = css({ + display: "flex", + flexDirection: "row", + gap: "[32px]", +}); + const sectionStyle = css({ display: "flex", flexDirection: "column", gap: "[12px]", + flex: "[1]", }); -const settingsContainerStyle = css({ +const sectionTitleStyle = css({ + fontSize: "[11px]", + fontWeight: "[600]", + textTransform: "uppercase", + color: "[rgba(0, 0, 0, 0.5)]", + letterSpacing: "[0.5px]", + marginBottom: "[4px]", +}); + +const settingsRowStyle = css({ display: "flex", flexDirection: "row", gap: "[24px]", @@ -22,7 +39,7 @@ const settingGroupStyle = css({ display: "flex", flexDirection: "column", gap: "[4px]", - minWidth: "[150px]", + minWidth: "[120px]", }); const labelStyle = css({ @@ -37,65 +54,110 @@ const smallLabelStyle = css({ }); const inputStyle = css({ - fontSize: "[14px]", - padding: "[6px 8px]", + fontSize: "[13px]", + padding: "[5px 8px]", border: "[1px solid rgba(0, 0, 0, 0.1)]", borderRadius: "[4px]", backgroundColor: "[white]", - width: "[120px]", + width: "[100px]", }); const inputDisabledStyle = css({ - fontSize: "[14px]", - padding: "[6px 8px]", + fontSize: "[13px]", + padding: "[5px 8px]", border: "[1px solid rgba(0, 0, 0, 0.1)]", borderRadius: "[4px]", backgroundColor: "[rgba(0, 0, 0, 0.05)]", cursor: "not-allowed", - width: "[120px]", + width: "[100px]", }); const selectStyle = css({ - fontSize: "[14px]", - padding: "[6px 8px]", + fontSize: "[13px]", + padding: "[5px 8px]", border: "[1px solid rgba(0, 0, 0, 0.1)]", borderRadius: "[4px]", backgroundColor: "[white]", cursor: "pointer", - width: "[120px]", + width: "[100px]", }); const selectDisabledStyle = css({ - fontSize: "[14px]", - padding: "[6px 8px]", + fontSize: "[13px]", + padding: "[5px 8px]", border: "[1px solid rgba(0, 0, 0, 0.1)]", borderRadius: "[4px]", backgroundColor: "[rgba(0, 0, 0, 0.05)]", cursor: "not-allowed", - width: "[120px]", + width: "[100px]", }); -const stateContainerStyle = css({ +const parametersListStyle = css({ display: "flex", flexDirection: "column", - gap: "[4px]", - padding: "[8px 12px]", - backgroundColor: "[rgba(0, 0, 0, 0.03)]", + gap: "[6px]", +}); + +const parameterRowStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "[6px 10px]", + backgroundColor: "[rgba(0, 0, 0, 0.02)]", borderRadius: "[4px]", }); -const stateLabelStyle = css({ +const parameterNameStyle = css({ + fontSize: "[13px]", + color: "[#333]", +}); + +const parameterVarNameStyle = css({ fontSize: "[11px]", - fontWeight: "[600]", - textTransform: "uppercase", - color: "[rgba(0, 0, 0, 0.5)]", - letterSpacing: "[0.5px]", + color: "[#6b7280]", + fontFamily: "[monospace]", +}); + +const parameterInputStyle = css({ + padding: "[4px 8px]", + fontSize: "[13px]", + borderRadius: "[4px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + backgroundColor: "[white]", + width: "[80px]", + textAlign: "right", +}); + +const parameterInputDisabledStyle = css({ + padding: "[4px 8px]", + fontSize: "[13px]", + borderRadius: "[4px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + width: "[80px]", + textAlign: "right", + cursor: "not-allowed", +}); + +const emptyMessageStyle = css({ + fontSize: "[12px]", + color: "[#9ca3af]", + fontStyle: "italic", +}); + +const errorContainerStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[4px]", + padding: "[8px 12px]", + backgroundColor: "[rgba(211, 47, 47, 0.05)]", + borderRadius: "[4px]", + marginTop: "[8px]", }); const errorTextStyle = css({ fontSize: "[11px]", color: "[#d32f2f]", - marginTop: "[4px]", maxWidth: "[400px]", wordWrap: "break-word", userSelect: "text", @@ -124,6 +186,7 @@ const editButtonIconStyle = css({ /** * SimulationSettingsContent displays simulation settings in the BottomPanel. + * Split into two sections: Computation and Parameters. */ export const SimulationSettingsContent: React.FC = () => { const setGlobalMode = useEditorStore((state) => state.setGlobalMode); @@ -135,6 +198,14 @@ export const SimulationSettingsContent: React.FC = () => { const setSelectedResourceId = useEditorStore( (state) => state.setSelectedResourceId ); + const parameterValues = useSimulationStore((state) => state.parameterValues); + const setParameterValue = useSimulationStore( + (state) => state.setParameterValue + ); + + const { + petriNetDefinition: { parameters }, + } = useSDCPNContext(); // Local state for ODE solver (not used in simulation yet, but UI is ready) const [odeSolver, setOdeSolver] = useState("euler"); @@ -143,52 +214,96 @@ export const SimulationSettingsContent: React.FC = () => { simulationState === "Running" || simulationState === "Paused"; return ( -
- {/* Settings Row */} -
- {/* Time Step Input */} -
- - { - const value = Number.parseFloat(event.target.value); - if (value > 0) { - setDt(value); - } - }} - disabled={isSimulationActive} - className={isSimulationActive ? inputDisabledStyle : inputStyle} - /> +
+
+ {/* Computation Section */} +
+
Computation
+
+ {/* Time Step Input */} +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + { + const value = Number.parseFloat(event.target.value); + if (value > 0) { + setDt(value); + } + }} + disabled={isSimulationActive} + className={isSimulationActive ? inputDisabledStyle : inputStyle} + /> +
+ {/* ODE Solver Method Select */} +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + +
+
- {/* ODE Solver Method Select */} -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control -- it can't tell it's the same ID */} - - + + {/* Parameters Section */} +
+
Parameters
+ {parameters.length > 0 ? ( +
+ {parameters.map((param) => ( +
+
+
{param.name}
+
+ {param.variableName} +
+
+ + setParameterValue(param.variableName, event.target.value) + } + placeholder={param.defaultValue} + disabled={isSimulationActive} + className={ + isSimulationActive + ? parameterInputDisabledStyle + : parameterInputStyle + } + /> +
+ ))} +
+ ) : ( +
No parameters defined
+ )}
{/* Error Display */} {simulationState === "Error" && simulationError && ( -
+
{simulationError}
{errorItemId && ( - ))} +
+ {tabs.map((tab) => ( + + ))} +
+
{/* Scrollable content */} From 8d28fabaac0d32ed54822a1da44f3376e076fdbe Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 10:49:32 +0100 Subject: [PATCH 26/40] Diagnostics Indicator opens Diagnostics in BottomPanel, with Tooltip --- .../components/BottomBar/bottom-bar.tsx | 16 +++++-- .../BottomBar/diagnostics-indicator.tsx | 43 ++++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index 1d56fe01f42..e1971a6cc69 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -1,6 +1,6 @@ import { css } from "@hashintel/ds-helpers/css"; import { refractive } from "@hashintel/refractive"; -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { useCheckerContext } from "../../../../state/checker-provider"; import { useEditorStore } from "../../../../state/editor-provider"; @@ -52,12 +52,22 @@ export const BottomBar: React.FC = ({ onEditionModeChange, }) => { const isBottomPanelOpen = useEditorStore((state) => state.isBottomPanelOpen); - const toggleBottomPanel = useEditorStore((state) => state.toggleBottomPanel); + const setBottomPanelOpen = useEditorStore( + (state) => state.setBottomPanelOpen + ); + const setActiveBottomPanelTab = useEditorStore( + (state) => state.setActiveBottomPanelTab + ); const bottomPanelHeight = useEditorStore((state) => state.bottomPanelHeight); const { totalDiagnosticsCount } = useCheckerContext(); const hasDiagnostics = totalDiagnosticsCount > 0; + const showDiagnostics = useCallback(() => { + setBottomPanelOpen(true); + setActiveBottomPanelTab("diagnostics"); + }, [setBottomPanelOpen, setActiveBottomPanelTab]); + // Fallback to 'pan' mode when switching to simulate mode if mutative mode useEffect(() => { if ( @@ -89,7 +99,7 @@ export const BottomBar: React.FC = ({ >
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/diagnostics-indicator.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/diagnostics-indicator.tsx index 033d6d2db7d..c9cdaa76746 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/diagnostics-indicator.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/diagnostics-indicator.tsx @@ -1,6 +1,7 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { FaCheck, FaXmark } from "react-icons/fa6"; +import { Tooltip } from "../../../../components/tooltip"; import { useCheckerContext } from "../../../../state/checker-provider"; const buttonStyle = cva({ @@ -60,25 +61,27 @@ export const DiagnosticsIndicator: React.FC = ({ const hasErrors = totalDiagnosticsCount > 0; return ( - + + + ); }; From 914d97b0bfdad5a0f7a7ab67d2e2c7a4d7c184ef Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 11:06:37 +0100 Subject: [PATCH 27/40] Add Toggle Panel button in BottomBar --- .../components/BottomBar/bottom-bar.tsx | 40 +++++++++++++++++++ .../BottomBar/diagnostics-indicator.tsx | 3 +- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index e1971a6cc69..aadbf31eb52 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -1,7 +1,9 @@ import { css } from "@hashintel/ds-helpers/css"; import { refractive } from "@hashintel/refractive"; import { useCallback, useEffect } from "react"; +import { FaChevronDown, FaChevronUp } from "react-icons/fa6"; +import { Tooltip } from "../../../../components/tooltip"; import { useCheckerContext } from "../../../../state/checker-provider"; import { useEditorStore } from "../../../../state/editor-provider"; import type { EditorState } from "../../../../state/editor-store"; @@ -30,6 +32,25 @@ const dividerStyle = css({ margin: "[0 4px]", }); +const panelToggleButtonStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "[32px]", + height: "[32px]", + border: "none", + borderRadius: "[8px]", + backgroundColor: "[transparent]", + color: "core.gray.70", + cursor: "pointer", + transition: "[all 0.2s ease]", + marginLeft: "[4px]", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + color: "core.gray.90", + }, +}); + const bottomBarPositionStyle = css({ position: "fixed", left: "[50%]", @@ -68,6 +89,10 @@ export const BottomBar: React.FC = ({ setActiveBottomPanelTab("diagnostics"); }, [setBottomPanelOpen, setActiveBottomPanelTab]); + const toggleBottomPanel = useCallback(() => { + setBottomPanelOpen(!isBottomPanelOpen); + }, [setBottomPanelOpen, isBottomPanelOpen]); + // Fallback to 'pan' mode when switching to simulate mode if mutative mode useEffect(() => { if ( @@ -98,6 +123,21 @@ export const BottomBar: React.FC = ({ }} >
+ + + Date: Thu, 18 Dec 2025 11:25:43 +0100 Subject: [PATCH 28/40] Hide Place/Transition buttons in BottomBar when Simulation is running --- .../Editor/components/BottomBar/toolbar-modes.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx index 4406b19ca3a..31a3a477ae9 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx @@ -3,6 +3,7 @@ import { FaArrowPointer, FaCircle, FaHand, FaSquare } from "react-icons/fa6"; import { Tooltip } from "../../../../components/tooltip"; import type { EditorState } from "../../../../state/editor-store"; +import { useSimulationStore } from "../../../../state/simulation-provider"; type EditorMode = EditorState["globalMode"]; type EditorEditionMode = EditorState["editionMode"]; @@ -62,9 +63,16 @@ export const ToolbarModes: React.FC = ({ editionMode, onEditionModeChange, }) => { + const simulationState = useSimulationStore((state) => state.state); + const isSimulationRunning = + simulationState === "Running" || simulationState === "Paused"; + + // Show Place/Transition buttons only in edit mode and when simulation is not running + const showMutativeButtons = mode === "edit" && !isSimulationRunning; + return ( <> - {mode === "edit" && ( + {showMutativeButtons && ( <>
Date: Thu, 18 Dec 2025 11:31:37 +0100 Subject: [PATCH 29/40] SDCPNView readonly when Simulation is running --- .../petrinaut/src/views/SDCPN/sdcpn-view.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index 381b7132498..a63ccc59eb1 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -12,6 +12,7 @@ import { } from "../../core/default-codes"; import { useEditorStore } from "../../state/editor-provider"; import { useSDCPNContext } from "../../state/sdcpn-provider"; +import { useSimulationStore } from "../../state/simulation-provider"; import type { ArcData, NodeData } from "../../state/types-for-editor-to-remove"; import { Arc } from "./components/arc"; import { PlaceNode } from "./components/place-node"; @@ -65,13 +66,16 @@ export const SDCPNView: React.FC = () => { const setEditionMode = useEditorStore((state) => state.setEditionMode); const selectedItemIds = useEditorStore((state) => state.selectedItemIds); const setSelectedItemIds = useEditorStore( - (state) => state.setSelectedItemIds, + (state) => state.setSelectedItemIds ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); const clearSelection = useEditorStore((state) => state.clearSelection); + // Simulation state + const simulationState = useSimulationStore((state) => state.state); + // Center viewport on SDCPN load useEffect(() => { if (reactFlowInstance) { @@ -79,8 +83,10 @@ export const SDCPNView: React.FC = () => { } }, [reactFlowInstance, petriNetId]); - // Readonly if in simulate mode, or readonly has been provided by external consumer. - const isReadonly = mode === "simulate" || readonly; + // 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; function isValidConnection(connection: Connection) { const sourceNode = nodes.find((node) => node.id === connection.source); @@ -129,7 +135,7 @@ export const SDCPNView: React.FC = () => { // Shared function to create a node at a given position function createNodeAtPosition( nodeType: "place" | "transition", - position: { x: number; y: number }, + position: { x: number; y: number } ) { const { width, height } = nodeDimensions[nodeType]; From c35739ea465a7f26f4ebc99b17b2bb9c91852f50 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 11:52:01 +0100 Subject: [PATCH 30/40] Make Token Types, Differential Equations and Global Parameters readonly while Simulation is running --- .../BottomPanel/parameters-content.tsx | 22 +++++ .../differential-equations-section.tsx | 24 +++++ .../components/LeftSideBar/types-section.tsx | 24 +++++ .../PropertiesPanel/parameter-properties.tsx | 8 +- .../PropertiesPanel/place-properties.tsx | 32 +++--- .../PropertiesPanel/transition-properties.tsx | 97 ++++++++++--------- .../PropertiesPanel/type-properties.tsx | 24 +++-- 7 files changed, 161 insertions(+), 70 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx index c09e4f96098..b8a4ce3a9e4 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx @@ -38,6 +38,14 @@ const addButtonStyle = css({ backgroundColor: "[rgba(0, 0, 0, 0.05)]", color: "core.gray.90", }, + _disabled: { + cursor: "not-allowed", + opacity: "[0.4]", + _hover: { + backgroundColor: "[transparent]", + color: "core.gray.60", + }, + }, }); const parameterRowStyle = css({ @@ -84,6 +92,14 @@ const deleteButtonStyle = css({ backgroundColor: "[rgba(255, 0, 0, 0.1)]", color: "[#ef4444]", }, + _disabled: { + cursor: "not-allowed", + opacity: "[0.3]", + _hover: { + backgroundColor: "[transparent]", + color: "core.gray.50", + }, + }, }); const emptyMessageStyle = css({ @@ -119,6 +135,10 @@ export const ParametersContent: React.FC = () => { globalMode === "simulate" && simulationState === "NotRun"; const isSimulationMode = globalMode === "simulate"; + // Check if simulation is running or paused + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + const handleAddParameter = () => { const name = `param${parameters.length + 1}`; const id = uuidv4(); @@ -141,6 +161,7 @@ export const ParametersContent: React.FC = () => { {!isSimulationMode && (
= ({ onClick: () => { updateTransition(transition.id, (existingTransition) => { existingTransition.lambdaCode = generateDefaultLambdaCode( - existingTransition.lambdaType, + existingTransition.lambdaType ); }); }, @@ -447,15 +454,14 @@ export const TransitionProperties: React.FC = ({ borderRadius: 4, overflow: "hidden", height: 340, - filter: - globalMode === "simulate" - ? "grayscale(20%) brightness(98%)" - : "none", - pointerEvents: globalMode === "simulate" ? "none" : "auto", + filter: isReadOnly ? "grayscale(20%) brightness(98%)" : "none", + pointerEvents: isReadOnly ? "none" : "auto", }} > `${a.placeId}:${a.weight}`).join("-")}`} + key={`lambda-${transition.lambdaType}-${transition.inputArcs + .map((a) => `${a.placeId}:${a.weight}`) + .join("-")}`} language="typescript" value={transition.lambdaCode || ""} path={`inmemory://sdcpn/transitions/${transition.id}/lambda.ts`} @@ -475,7 +481,7 @@ export const TransitionProperties: React.FC = ({ lineDecorationsWidth: 0, lineNumbersMinChars: 3, padding: { top: 8, bottom: 8 }, - readOnly: globalMode === "simulate", + readOnly: isReadOnly, fixedOverflowWidgets: true, }} /> @@ -527,11 +533,11 @@ export const TransitionProperties: React.FC = ({ const inputs = transition.inputArcs .map((arc) => { const place = places.find( - (p) => p.id === arc.placeId, + (p) => p.id === arc.placeId ); if (!place || !place.colorId) return null; const type = types.find( - (t) => t.id === place.colorId, + (t) => t.id === place.colorId ); if (!type) return null; return { @@ -545,11 +551,11 @@ export const TransitionProperties: React.FC = ({ const outputs = transition.outputArcs .map((arc) => { const place = places.find( - (p) => p.id === arc.placeId, + (p) => p.id === arc.placeId ); if (!place || !place.colorId) return null; const type = types.find( - (t) => t.id === place.colorId, + (t) => t.id === place.colorId ); if (!type) return null; return { @@ -597,15 +603,16 @@ export const TransitionProperties: React.FC = ({ borderRadius: 4, overflow: "hidden", height: 400, - filter: - globalMode === "simulate" - ? "grayscale(20%) brightness(98%)" - : "none", - pointerEvents: globalMode === "simulate" ? "none" : "auto", + filter: isReadOnly ? "grayscale(20%) brightness(98%)" : "none", + pointerEvents: isReadOnly ? "none" : "auto", }} > `${a.placeId}:${a.weight}`).join("-")}-${transition.outputArcs.map((a) => `${a.placeId}:${a.weight}`).join("-")}`} + key={`kernel-${transition.inputArcs + .map((a) => `${a.placeId}:${a.weight}`) + .join("-")}-${transition.outputArcs + .map((a) => `${a.placeId}:${a.weight}`) + .join("-")}`} language="typescript" value={transition.transitionKernelCode || ""} path={`inmemory://sdcpn/transitions/${transition.id}/transition-kernel.ts`} @@ -625,7 +632,7 @@ export const TransitionProperties: React.FC = ({ lineDecorationsWidth: 0, lineNumbersMinChars: 3, padding: { top: 8, bottom: 8 }, - readOnly: globalMode === "simulate", + readOnly: isReadOnly, fixedOverflowWidgets: true, }} /> diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx index bd5b8decae4..c556a7195ad 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; import type { Color } from "../../../../core/types/sdcpn"; +import { useSimulationStore } from "../../../../state/simulation-provider"; import { ColorSelect } from "./color-select"; /** @@ -47,7 +48,12 @@ export const TypeProperties: React.FC = ({ updateType, globalMode, }) => { - const isDisabled = globalMode === "simulate"; + const simulationState = useSimulationStore((state) => state.state); + + // Check if simulation is running or paused + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + const isDisabled = globalMode === "simulate" || isSimulationActive; const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); @@ -95,13 +101,13 @@ export const TypeProperties: React.FC = ({ // Check for duplicates (excluding the current element) const isDuplicate = type.elements.some( - (elem) => elem.elementId !== elementId && elem.name === slugifiedName, + (elem) => elem.elementId !== elementId && elem.name === slugifiedName ); if (isDuplicate) { // eslint-disable-next-line no-alert alert( - `Warning: An element named "${slugifiedName}" already exists. Please use a unique name.`, + `Warning: An element named "${slugifiedName}" already exists. Please use a unique name.` ); return; } @@ -123,7 +129,7 @@ export const TypeProperties: React.FC = ({ // Confirmation dialog using browser API // eslint-disable-next-line no-alert const confirmed = window.confirm( - `Delete element "${elementName}"?\n\nThis cannot be undone.`, + `Delete element "${elementName}"?\n\nThis cannot be undone.` ); if (!confirmed) { @@ -132,7 +138,7 @@ export const TypeProperties: React.FC = ({ updateType(type.id, (existingType) => { const index = existingType.elements.findIndex( - (elem) => elem.elementId === elementId, + (elem) => elem.elementId === elementId ); if (index !== -1) { existingType.elements.splice(index, 1); @@ -314,8 +320,8 @@ export const TypeProperties: React.FC = ({ draggedIndex === index ? "rgba(59, 130, 246, 0.1)" : dragOverIndex === index - ? "rgba(59, 130, 246, 0.05)" - : "rgba(0, 0, 0, 0.03)", + ? "rgba(59, 130, 246, 0.05)" + : "rgba(0, 0, 0, 0.03)", borderRadius: 3, border: dragOverIndex === index @@ -387,13 +393,13 @@ export const TypeProperties: React.FC = ({ onChange={(event) => { handleUpdateElementName( element.elementId, - event.target.value, + event.target.value ); }} onBlur={(event) => { handleBlurElementName( element.elementId, - event.target.value, + event.target.value ); }} disabled={isDisabled} From d7d27dcaf161194b794f654f36d5ccc9f590ae38 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Thu, 18 Dec 2025 14:57:26 +0100 Subject: [PATCH 31/40] Make Diagnostics expanded by default --- .../Editor/components/BottomPanel/diagnostics-content.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx index 7b3be992c5e..7adbd1239be 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx @@ -93,7 +93,8 @@ export const DiagnosticsContent: React.FC = () => { const setSelectedResourceId = useEditorStore( (state) => state.setSelectedResourceId ); - const [expandedEntities, setExpandedEntities] = useState>( + // Track collapsed entities (all expanded by default) + const [collapsedEntities, setCollapsedEntities] = useState>( new Set() ); @@ -154,7 +155,7 @@ export const DiagnosticsContent: React.FC = () => { }, [checkResult, petriNetDefinition]); const toggleEntity = useCallback((entityKey: string) => { - setExpandedEntities((prev) => { + setCollapsedEntities((prev) => { const next = new Set(prev); if (next.has(entityKey)) { next.delete(entityKey); @@ -175,7 +176,7 @@ export const DiagnosticsContent: React.FC = () => { <> {groupedDiagnostics.map((group) => { const entityKey = `${group.entityType}:${group.entityId}`; - const isExpanded = expandedEntities.has(entityKey); + const isExpanded = !collapsedEntities.has(entityKey); const entityLabel = group.entityType === "transition" ? `Transition: ${group.entityName}` From 8afa254a3eff94ba67f3693e105f543ed38c331e Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Fri, 19 Dec 2025 02:32:47 +0100 Subject: [PATCH 32/40] Toolbar refinement + updates from demo review --- .../components/BottomBar/bottom-bar.tsx | 55 ++---- .../BottomBar/diagnostics-indicator.tsx | 48 +++-- .../BottomBar/simulation-controls.tsx | 154 ++++++--------- .../components/BottomBar/toolbar-button.tsx | 108 +++++++++++ .../components/BottomBar/toolbar-modes.tsx | 178 +++++------------- 5 files changed, 250 insertions(+), 293 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index aadbf31eb52..b1aff990e9e 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -3,12 +3,12 @@ import { refractive } from "@hashintel/refractive"; import { useCallback, useEffect } from "react"; import { FaChevronDown, FaChevronUp } from "react-icons/fa6"; -import { Tooltip } from "../../../../components/tooltip"; import { useCheckerContext } from "../../../../state/checker-provider"; import { useEditorStore } from "../../../../state/editor-provider"; import type { EditorState } from "../../../../state/editor-store"; import { DiagnosticsIndicator } from "./diagnostics-indicator"; import { SimulationControls } from "./simulation-controls"; +import { ToolbarButton } from "./toolbar-button"; import { ToolbarModes } from "./toolbar-modes"; import { useKeyboardShortcuts } from "./use-keyboard-shortcuts"; @@ -16,7 +16,12 @@ const refractiveContainerStyle = css({ paddingX: "spacing.2", paddingY: "spacing.1", backgroundColor: "[rgba(255, 255, 255, 0.6)]", - boxShadow: "[0 4px 13px rgba(0, 0, 0, 0.15)]", + boxShadow: "[0 3px 11px rgba(0, 0, 0, 0.1)]", + transition: "[all 0.3s ease]", + _hover: { + backgroundColor: "[rgba(255, 255, 255, 0.8)]", + boxShadow: "[0 4px 13px rgba(0, 0, 0, 0.15)]", + }, }); const toolbarContainerStyle = css({ @@ -32,25 +37,6 @@ const dividerStyle = css({ margin: "[0 4px]", }); -const panelToggleButtonStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[32px]", - height: "[32px]", - border: "none", - borderRadius: "[8px]", - backgroundColor: "[transparent]", - color: "core.gray.70", - cursor: "pointer", - transition: "[all 0.2s ease]", - marginLeft: "[4px]", - _hover: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - color: "core.gray.90", - }, -}); - const bottomBarPositionStyle = css({ position: "fixed", left: "[50%]", @@ -123,21 +109,18 @@ export const BottomBar: React.FC = ({ }} >
- - - + + {isBottomPanelOpen ? ( + + ) : ( + + )} + = ({ const hasErrors = totalDiagnosticsCount > 0; return ( - - - +
+ ); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 7df01e342fb..874b8d6ba74 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -1,32 +1,10 @@ import { css } from "@hashintel/ds-helpers/css"; -import { IoMdPause, IoMdPlay, IoMdSquare } from "react-icons/io"; +import { IoMdPause, IoMdPlay } from "react-icons/io"; +import { MdRotateLeft } from "react-icons/md"; -import { Tooltip } from "../../../../components/tooltip"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; - -const containerStyle = css({ - display: "flex", - alignItems: "center", - padding: "[0 12px]", - gap: "[12px]", - fontSize: "[24px]", -}); - -const playPauseButtonStyle = css({ - cursor: "pointer", - display: "flex", - alignItems: "center", - justifyContent: "center", - border: "none", - transition: "[all 0.2s ease]", - "&:hover:not([data-disabled])": { - transform: "[scale(1.05)]", - }, - "&[data-disabled]": { - opacity: "[0.5]", - }, -}); +import { ToolbarButton } from "./toolbar-button"; const frameInfoStyle = css({ display: "flex", @@ -35,6 +13,7 @@ const frameInfoStyle = css({ fontSize: "[11px]", color: "core.gray.60", fontWeight: "[500]", + lineHeight: "[1]", minWidth: "[80px]", }); @@ -74,28 +53,6 @@ const sliderStyle = css({ }, }); -const resetButtonStyle = css({ - cursor: "pointer", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[36px]", - height: "[36px]", - borderRadius: "[6px]", - border: "none", - background: "[transparent]", - color: "core.gray.80", - transition: "[all 0.2s ease]", - "&:hover:not(:disabled)": { - background: "core.gray.10", - transform: "[scale(1.05)]", - }, - "&:disabled": { - opacity: "[0.5]", - cursor: "not-allowed", - }, -}); - interface SimulationControlsProps { disabled?: boolean; } @@ -126,16 +83,42 @@ export const SimulationControls: React.FC = ({ const isDisabled = disabled; - function openDiagnosticsPanel() { + const openDiagnosticsPanel = () => { setActiveBottomPanelTab("diagnostics"); setBottomPanelOpen(true); - } + }; const totalFrames = simulation?.frames.length ?? 0; const hasSimulation = simulation !== null; const isRunning = simulationState === "Running"; const elapsedTime = simulation ? currentlyViewedFrame * simulation.dt : 0; + const getPlayPauseTooltip = () => { + if (isDisabled) { + return "Fix errors to run simulation"; + } + if (simulationState === "NotRun") { + return "Start Simulation"; + } + if (isRunning) { + return "Pause Simulation"; + } + return "Continue Simulation"; + }; + + const getPlayPauseAriaLabel = () => { + if (isDisabled) { + return "Fix errors to run simulation"; + } + if (simulationState === "NotRun") { + return "Run simulation"; + } + if (isRunning) { + return "Pause simulation"; + } + return "Continue simulation"; + }; + const handlePlayPause = () => { // If disabled due to errors, open diagnostics panel instead if (isDisabled) { @@ -167,63 +150,28 @@ export const SimulationControls: React.FC = ({ }; return ( -
- {/* Record/Stop button - always visible */} - + {/* Play/Pause button - always visible */} + - - - - {/* Reset button - only visible when simulation exists */} - {hasSimulation && ( - - - - )} + {isRunning ? : } + {/* Frame controls - only visible when simulation exists */} {hasSimulation && ( <> - +
Frame
{currentlyViewedFrame + 1} / {totalFrames}
{elapsedTime.toFixed(3)}s
- +
+ = ({ /> )} -
+ + {/* Stop button - only visible when simulation exists */} + {hasSimulation && ( + + + + )} + ); }; 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 new file mode 100644 index 00000000000..e2571f94769 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx @@ -0,0 +1,108 @@ +import { cva } from "@hashintel/ds-helpers/css"; +import type { ReactNode } from "react"; + +import { Tooltip } from "../../../../components/tooltip"; + +const buttonStyle = cva({ + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + border: "none", + borderRadius: "[8px]", + cursor: "pointer", + transition: "[all 0.2s ease]", + backgroundColor: "[transparent]", + color: "core.gray.70", + width: "[40px]", + height: "[40px]", + fontSize: "[20px]", + _hover: { + transform: "[scale(1.1)]", + color: "core.gray.90", + }, + _active: { + transform: "[scale(0.95)]", + color: "core.gray.90", + }, + }, + variants: { + isSelected: { + true: { + color: "[#3b82f6]", + _hover: { + color: "[#2563eb]", + }, + }, + }, + isDisabled: { + true: { + opacity: "[0.4]", + }, + }, + }, +}); + +interface ToolbarButtonProps { + /** Tooltip content shown on hover */ + tooltip: string; + /** Click handler */ + onClick?: () => void; + /** Button content (icons, text, etc.) */ + children: ReactNode; + /** Whether the button is in a selected state */ + isSelected?: boolean; + /** Whether the button appears disabled (lower opacity, but still clickable) */ + disabled?: boolean; + /** Accessibility label */ + ariaLabel: string; + /** Accessibility expanded state */ + ariaExpanded?: boolean; + /** Whether the button is draggable */ + draggable?: boolean; + /** Drag start handler */ + onDragStart?: (event: React.DragEvent) => void; +} + +/** + * Unified button component for the BottomBar toolbar. + * Provides consistent styling, tooltip, and accessibility across all toolbar buttons. + */ +export const ToolbarButton: React.FC = ({ + tooltip, + onClick, + children, + isSelected = false, + disabled = false, + ariaLabel, + ariaExpanded, + draggable = false, + onDragStart, +}) => { + const handleKeyDown = (event: React.KeyboardEvent) => { + if ((event.key === "Enter" || event.key === " ") && onClick) { + event.preventDefault(); + onClick(); + } + }; + + return ( + + + + ); +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx index 31a3a477ae9..fccedbe178f 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-modes.tsx @@ -1,57 +1,12 @@ -import { cva } from "@hashintel/ds-helpers/css"; import { FaArrowPointer, FaCircle, FaHand, FaSquare } from "react-icons/fa6"; -import { Tooltip } from "../../../../components/tooltip"; import type { EditorState } from "../../../../state/editor-store"; import { useSimulationStore } from "../../../../state/simulation-provider"; +import { ToolbarButton } from "./toolbar-button"; type EditorMode = EditorState["globalMode"]; type EditorEditionMode = EditorState["editionMode"]; -const iconContainerStyle = cva({ - base: { - cursor: "pointer", - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[45px]", - height: "[45px]", - fontSize: "[22px]", - transition: "[all 0.2s ease]", - _hover: { - transform: "[scale(1.1)]", - }, - }, - variants: { - selected: { - true: { - color: "[#3b82f6]", - }, - false: { - color: "core.gray.70", - }, - }, - }, - compoundVariants: [ - { - selected: true, - css: { - _hover: { - color: "[#2563eb]", - }, - }, - }, - { - selected: false, - css: { - _hover: { - color: "core.gray.90", - }, - }, - }, - ], -}); - interface ToolbarModesProps { mode: EditorMode; editionMode: EditorEditionMode; @@ -74,95 +29,52 @@ export const ToolbarModes: React.FC = ({ <> {showMutativeButtons && ( <> - -
onEditionModeChange("add-place")} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onEditionModeChange("add-place"); - } - }} - draggable - onDragStart={(event) => { - // eslint-disable-next-line no-param-reassign - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("application/reactflow", "place"); - }} - role="button" - tabIndex={0} - aria-label="Add place mode" - > - -
-
- -
onEditionModeChange("add-transition")} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onEditionModeChange("add-transition"); - } - }} - draggable - onDragStart={(event) => { - // eslint-disable-next-line no-param-reassign - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData( - "application/reactflow", - "transition", - ); - }} - role="button" - tabIndex={0} - aria-label="Add transition mode" - > - -
-
+ onEditionModeChange("add-place")} + isSelected={editionMode === "add-place"} + ariaLabel="Add place mode" + draggable + onDragStart={(event) => { + // eslint-disable-next-line no-param-reassign + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("application/reactflow", "place"); + }} + > + + + onEditionModeChange("add-transition")} + isSelected={editionMode === "add-transition"} + ariaLabel="Add transition mode" + draggable + onDragStart={(event) => { + // eslint-disable-next-line no-param-reassign + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("application/reactflow", "transition"); + }} + > + + )} - -
onEditionModeChange("select")} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onEditionModeChange("select"); - } - }} - role="button" - tabIndex={0} - aria-label="Select mode" - > - -
-
- -
onEditionModeChange("pan")} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onEditionModeChange("pan"); - } - }} - role="button" - tabIndex={0} - aria-label="Pan mode" - > - -
-
+ onEditionModeChange("select")} + isSelected={editionMode === "select"} + ariaLabel="Select mode" + > + + + onEditionModeChange("pan")} + isSelected={editionMode === "pan"} + ariaLabel="Pan mode" + > + + ); }; From a31496091b753c56ec0c8c9e133d14b8a81cf077 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Fri, 19 Dec 2025 02:35:20 +0100 Subject: [PATCH 33/40] Adjust bezelWidth for current BottomBar height --- .../src/views/Editor/components/BottomBar/bottom-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index b1aff990e9e..3619f3f3b00 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -104,7 +104,7 @@ export const BottomBar: React.FC = ({ refraction={{ radius: 12, blur: 3, - bezelWidth: 22, + bezelWidth: 20, glassThickness: 100, }} > From b41dd58a9b40e65b98e4c9795d517ccbf99234d4 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Fri, 19 Dec 2025 02:42:43 +0100 Subject: [PATCH 34/40] Swap Parameters and Computation sections --- .../simulation-settings-content.tsx | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx index 120b39e854a..47ec439f406 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -216,6 +216,43 @@ export const SimulationSettingsContent: React.FC = () => { return (
+ {/* Parameters Section */} +
+
Parameters
+ {parameters.length > 0 ? ( +
+ {parameters.map((param) => ( +
+
+
{param.name}
+
+ {param.variableName} +
+
+ + setParameterValue(param.variableName, event.target.value) + } + placeholder={param.defaultValue} + disabled={isSimulationActive} + className={ + isSimulationActive + ? parameterInputDisabledStyle + : parameterInputStyle + } + /> +
+ ))} +
+ ) : ( +
No parameters defined
+ )} +
+ {/* Computation Section */}
Computation
@@ -262,43 +299,6 @@ export const SimulationSettingsContent: React.FC = () => {
- - {/* Parameters Section */} -
-
Parameters
- {parameters.length > 0 ? ( -
- {parameters.map((param) => ( -
-
-
{param.name}
-
- {param.variableName} -
-
- - setParameterValue(param.variableName, event.target.value) - } - placeholder={param.defaultValue} - disabled={isSimulationActive} - className={ - isSimulationActive - ? parameterInputDisabledStyle - : parameterInputStyle - } - /> -
- ))} -
- ) : ( -
No parameters defined
- )} -
{/* Error Display */} From ca274c98f7f4507752102f171caa09194890ba1b Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Fri, 19 Dec 2025 02:47:13 +0100 Subject: [PATCH 35/40] Add tooltip on Time Step --- .../components/BottomPanel/simulation-settings-content.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx index 47ec439f406..72db04e78dc 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -2,6 +2,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { useState } from "react"; import { TbArrowRight } from "react-icons/tb"; +import { InfoIconTooltip } from "../../../../components/tooltip"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; @@ -262,6 +263,7 @@ export const SimulationSettingsContent: React.FC = () => { {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} Date: Fri, 19 Dec 2025 23:59:13 +0100 Subject: [PATCH 36/40] Format --- .../petrinaut/src/components/glass-panel.tsx | 8 ++-- .../petrinaut/src/state/editor-store.ts | 20 ++++----- .../components/BottomBar/bottom-bar.tsx | 4 +- .../BottomBar/simulation-controls.tsx | 8 ++-- .../components/BottomPanel/bottom-panel.tsx | 2 +- .../BottomPanel/diagnostics-content.tsx | 12 +++--- .../BottomPanel/parameters-content.tsx | 6 +-- .../simulation-settings-content.tsx | 4 +- .../components/LeftSideBar/left-sidebar.tsx | 4 +- .../differential-equation-properties.tsx | 18 ++++---- .../PropertiesPanel/place-properties.tsx | 22 +++++----- .../PropertiesPanel/properties-panel.tsx | 22 +++++----- .../PropertiesPanel/transition-properties.tsx | 42 +++++++++---------- .../PropertiesPanel/type-properties.tsx | 16 +++---- .../views/Editor/components/mode-selector.tsx | 2 +- .../src/views/Editor/editor-view.tsx | 2 +- .../src/views/SDCPN/components/place-node.tsx | 10 ++--- .../petrinaut/src/views/SDCPN/sdcpn-view.tsx | 6 +-- 18 files changed, 103 insertions(+), 105 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx index d28a8740b98..643419195f5 100644 --- a/libs/@hashintel/petrinaut/src/components/glass-panel.tsx +++ b/libs/@hashintel/petrinaut/src/components/glass-panel.tsx @@ -153,7 +153,7 @@ export const GlassPanel: React.FC = ({ resizeStartPosRef.current = isVertical ? event.clientY : event.clientX; resizeStartSizeRef.current = resizable.size; }, - [resizable] + [resizable], ); const handleResizeMove = useCallback( @@ -178,12 +178,12 @@ export const GlassPanel: React.FC = ({ const newSize = Math.max( minSize, - Math.min(maxSize, resizeStartSizeRef.current + delta) + Math.min(maxSize, resizeStartSizeRef.current + delta), ); onResize(newSize); }, - [isResizing, resizable] + [isResizing, resizable], ); const handleResizeEnd = useCallback(() => { @@ -218,7 +218,7 @@ export const GlassPanel: React.FC = ({ onResize(newSize); } }, - [resizable] + [resizable], ); // Global cursor and event listeners during resize diff --git a/libs/@hashintel/petrinaut/src/state/editor-store.ts b/libs/@hashintel/petrinaut/src/state/editor-store.ts index 55a6f47afdf..669b440e198 100644 --- a/libs/@hashintel/petrinaut/src/state/editor-store.ts +++ b/libs/@hashintel/petrinaut/src/state/editor-store.ts @@ -60,7 +60,7 @@ export type EditorState = { draggingStateByNodeId: DraggingStateByNodeId; setDraggingStateByNodeId: (state: DraggingStateByNodeId) => void; updateDraggingStateByNodeId: ( - updater: (state: DraggingStateByNodeId) => DraggingStateByNodeId + updater: (state: DraggingStateByNodeId) => DraggingStateByNodeId, ) => void; resetDraggingState: () => void; @@ -119,7 +119,7 @@ export function createEditorStore() { isBottomPanelOpen: !state.isBottomPanelOpen, }), false, - "toggleBottomPanel" + "toggleBottomPanel", ), bottomPanelHeight: DEFAULT_BOTTOM_PANEL_HEIGHT, setBottomPanelHeight: (height) => @@ -157,7 +157,7 @@ export function createEditorStore() { return { selectedItemIds: newSet }; }, false, - { type: "addSelectedItemId", id } + { type: "addSelectedItemId", id }, ), removeSelectedItemId: (id) => set( @@ -167,7 +167,7 @@ export function createEditorStore() { return { selectedItemIds: newSet }; }, false, - { type: "removeSelectedItemId", id } + { type: "removeSelectedItemId", id }, ), clearSelection: () => set({ selectedItemIds: new Set() }, false, "clearSelection"), @@ -178,7 +178,7 @@ export function createEditorStore() { set( { draggingStateByNodeId: state }, false, - "setDraggingStateByNodeId" + "setDraggingStateByNodeId", ), updateDraggingStateByNodeId: (updater) => set( @@ -186,7 +186,7 @@ export function createEditorStore() { draggingStateByNodeId: updater(state.draggingStateByNodeId), }), false, - "updateDraggingStateByNodeId" + "updateDraggingStateByNodeId", ), resetDraggingState: () => set({ draggingStateByNodeId: {} }, false, "resetDraggingState"), @@ -207,12 +207,12 @@ export function createEditorStore() { draggingStateByNodeId: {}, }, false, - { type: "initializeEditorStore" } + { type: "initializeEditorStore" }, ); }, // for some reason 'create' doesn't raise an error if a function in the type is missing - } satisfies EditorState), - { name: "Editor Store" } - ) + }) satisfies EditorState, + { name: "Editor Store" }, + ), ); } diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx index 3619f3f3b00..bfdfa02ca91 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/bottom-bar.tsx @@ -60,10 +60,10 @@ export const BottomBar: React.FC = ({ }) => { const isBottomPanelOpen = useEditorStore((state) => state.isBottomPanelOpen); const setBottomPanelOpen = useEditorStore( - (state) => state.setBottomPanelOpen + (state) => state.setBottomPanelOpen, ); const setActiveBottomPanelTab = useEditorStore( - (state) => state.setActiveBottomPanelTab + (state) => state.setActiveBottomPanelTab, ); const bottomPanelHeight = useEditorStore((state) => state.bottomPanelHeight); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx index 874b8d6ba74..1e4e47fbbad 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/simulation-controls.tsx @@ -68,17 +68,17 @@ export const SimulationControls: React.FC = ({ const pause = useSimulationStore((state) => state.pause); const dt = useSimulationStore((state) => state.dt); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame + (state) => state.currentlyViewedFrame, ); const setCurrentlyViewedFrame = useSimulationStore( - (state) => state.setCurrentlyViewedFrame + (state) => state.setCurrentlyViewedFrame, ); const setBottomPanelOpen = useEditorStore( - (state) => state.setBottomPanelOpen + (state) => state.setBottomPanelOpen, ); const setActiveBottomPanelTab = useEditorStore( - (state) => state.setActiveBottomPanelTab + (state) => state.setActiveBottomPanelTab, ); const isDisabled = disabled; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx index 58a0cb87f2e..47f665c1f86 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx @@ -107,7 +107,7 @@ export const BottomPanel: React.FC = () => { const leftSidebarWidth = useEditorStore((state) => state.leftSidebarWidth); const panelHeight = useEditorStore((state) => state.bottomPanelHeight); const setBottomPanelHeight = useEditorStore( - (state) => state.setBottomPanelHeight + (state) => state.setBottomPanelHeight, ); const activeTab = useEditorStore((state) => state.activeBottomPanelTab); const setActiveTab = useEditorStore((state) => state.setActiveBottomPanelTab); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx index 7adbd1239be..f0fc63e2083 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx @@ -11,7 +11,7 @@ import { useSDCPNContext } from "../../../../state/sdcpn-provider"; * Formats a TypeScript diagnostic message to a readable string */ const formatDiagnosticMessage = ( - messageText: string | ts.DiagnosticMessageChain + messageText: string | ts.DiagnosticMessageChain, ): string => { if (typeof messageText === "string") { return messageText; @@ -91,11 +91,11 @@ export const DiagnosticsContent: React.FC = () => { const { checkResult, totalDiagnosticsCount } = useCheckerContext(); const { petriNetDefinition } = useSDCPNContext(); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId + (state) => state.setSelectedResourceId, ); // Track collapsed entities (all expanded by default) const [collapsedEntities, setCollapsedEntities] = useState>( - new Set() + new Set(), ); // Handler to select an entity when clicking on a diagnostic @@ -103,7 +103,7 @@ export const DiagnosticsContent: React.FC = () => { (entityId: string) => { setSelectedResourceId(entityId); }, - [setSelectedResourceId] + [setSelectedResourceId], ); // Group diagnostics by entity (transition or differential equation) @@ -119,14 +119,14 @@ export const DiagnosticsContent: React.FC = () => { if (item.itemType === "differential-equation") { entityType = "differential-equation"; const de = petriNetDefinition.differentialEquations.find( - (deItem) => deItem.id === entityId + (deItem) => deItem.id === entityId, ); entityName = de?.name ?? entityId; subType = null; } else { entityType = "transition"; const transition = petriNetDefinition.transitions.find( - (tr) => tr.id === entityId + (tr) => tr.id === entityId, ); entityName = transition?.name ?? entityId; subType = item.itemType === "transition-lambda" ? "lambda" : "kernel"; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx index b8a4ce3a9e4..43ba335e3e1 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx @@ -121,14 +121,14 @@ export const ParametersContent: React.FC = () => { const globalMode = useEditorStore((state) => state.globalMode); const simulationState = useSimulationStore((state) => state.state); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId + (state) => state.selectedResourceId, ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId + (state) => state.setSelectedResourceId, ); const parameterValues = useSimulationStore((state) => state.parameterValues); const setParameterValue = useSimulationStore( - (state) => state.setParameterValue + (state) => state.setParameterValue, ); const isSimulationNotRun = diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx index 72db04e78dc..0bc779161cc 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx @@ -197,11 +197,11 @@ export const SimulationSettingsContent: React.FC = () => { const dt = useSimulationStore((state) => state.dt); const setDt = useSimulationStore((state) => state.setDt); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId + (state) => state.setSelectedResourceId, ); const parameterValues = useSimulationStore((state) => state.parameterValues); const setParameterValue = useSimulationStore( - (state) => state.setParameterValue + (state) => state.setParameterValue, ); const { diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx index dab5bc788c6..6986fa1b3ce 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx @@ -148,11 +148,11 @@ export const LeftSideBar: React.FC = ({ }) => { const isOpen = useEditorStore((state) => state.isLeftSidebarOpen); const setLeftSidebarOpen = useEditorStore( - (state) => state.setLeftSidebarOpen + (state) => state.setLeftSidebarOpen, ); const leftSidebarWidth = useEditorStore((state) => state.leftSidebarWidth); const setLeftSidebarWidth = useEditorStore( - (state) => state.setLeftSidebarWidth + (state) => state.setLeftSidebarWidth, ); return ( diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx index 75d759feddb..0f3f02fa1db 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx @@ -25,7 +25,7 @@ interface DifferentialEquationPropertiesProps { globalMode: "edit" | "simulate"; updateDifferentialEquation: ( equationId: string, - updateFn: (equation: DifferentialEquation) => void + updateFn: (equation: DifferentialEquation) => void, ) => void; } @@ -48,7 +48,7 @@ export const DifferentialEquationProperties: React.FC< const isReadOnly = globalMode === "simulate" || isSimulationRunning; const associatedType = types.find( - (type) => type.id === differentialEquation.colorId + (type) => type.id === differentialEquation.colorId, ); // Find places that use this differential equation @@ -74,7 +74,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.colorId = newTypeId; - } + }, ); } }; @@ -85,7 +85,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.colorId = pendingTypeId; - } + }, ); } setShowConfirmDialog(false); @@ -124,7 +124,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.name = event.target.value; - } + }, ); }} disabled={isReadOnly} @@ -392,7 +392,7 @@ export const DifferentialEquationProperties: React.FC< onClick: () => { // Get the associated type to generate appropriate default code const equationType = types.find( - (t) => t.id === differentialEquation.colorId + (t) => t.id === differentialEquation.colorId, ); updateDifferentialEquation( @@ -400,10 +400,10 @@ export const DifferentialEquationProperties: React.FC< (existingEquation) => { existingEquation.code = equationType ? generateDefaultDifferentialEquationCode( - equationType + equationType, ) : DEFAULT_DIFFERENTIAL_EQUATION_CODE; - } + }, ); }, }, @@ -451,7 +451,7 @@ export const DifferentialEquationProperties: React.FC< differentialEquation.id, (existingEquation) => { existingEquation.code = newCode ?? ""; - } + }, ); }} path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx index 25c5bb18a16..e80465afd75 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx @@ -57,15 +57,15 @@ export const PlaceProperties: React.FC = ({ const isReadOnly = globalMode === "simulate" || isSimulationActive; const initialMarking = useSimulationStore((state) => state.initialMarking); const setInitialMarking = useSimulationStore( - (state) => state.setInitialMarking + (state) => state.setInitialMarking, ); const parameterValues = useSimulationStore((state) => state.parameterValues); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame + (state) => state.currentlyViewedFrame, ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId + (state) => state.setSelectedResourceId, ); const { @@ -133,7 +133,7 @@ export const PlaceProperties: React.FC = ({ if (!isPascalCase(nameInputValue)) { setNameError( - "Name must be in PascalCase (e.g., MyPlaceName or Place2). Any numbers must appear at the end." + "Name must be in PascalCase (e.g., MyPlaceName or Place2). Any numbers must appear at the end.", ); return; } @@ -194,7 +194,7 @@ export const PlaceProperties: React.FC = ({ 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.` + `Are you sure you want to delete "${place.name}"? All arcs connected to this place will also be removed.`, ) ) { removePlace(place.id); @@ -385,8 +385,8 @@ export const PlaceProperties: React.FC = ({ {place.colorId !== null ? "Create a differential equation for the selected type in the left-hand sidebar first" : availableTypes.length === 0 - ? "Create a type in the left-hand sidebar first, then select it to enable dynamics." - : "Select a type to enable dynamics"} + ? "Create a type in the left-hand sidebar first, then select it to enable dynamics." + : "Select a type to enable dynamics"}
)}
@@ -504,7 +504,7 @@ export const PlaceProperties: React.FC = ({ onChange={(event) => { const count = Math.max( 0, - Number.parseInt(event.target.value, 10) || 0 + Number.parseInt(event.target.value, 10) || 0, ); setInitialMarking(place.id, { values: new Float64Array(0), // Empty array for places without type @@ -725,7 +725,7 @@ export const PlaceProperties: React.FC = ({ const { offset, count } = placeState; const placeSize = count * dimensions; const tokenValues = Array.from( - currentFrame.buffer.slice(offset, offset + placeSize) + currentFrame.buffer.slice(offset, offset + placeSize), ); // Format tokens as array of objects with named dimensions @@ -747,7 +747,7 @@ export const PlaceProperties: React.FC = ({ // Merge SimulationStore values with SDCPN defaults parameters = mergeParameterValues( parameterValues, - defaultParameterValues + defaultParameterValues, ); } else { // Use initial marking @@ -772,7 +772,7 @@ export const PlaceProperties: React.FC = ({ // Merge SimulationStore values with SDCPN defaults parameters = mergeParameterValues( parameterValues, - defaultParameterValues + defaultParameterValues, ); } diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx index 41b0b3b79ad..517042e3f69 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx @@ -21,11 +21,11 @@ import { TypeProperties } from "./type-properties"; */ export const PropertiesPanel: React.FC = () => { const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId + (state) => state.selectedResourceId, ); const globalMode = useEditorStore((state) => state.globalMode); const setPropertiesPanelWidth = useEditorStore( - (state) => state.setPropertiesPanelWidth + (state) => state.setPropertiesPanelWidth, ); const isBottomPanelOpen = useEditorStore((state) => state.isBottomPanelOpen); const bottomPanelHeight = useEditorStore((state) => state.bottomPanelHeight); @@ -42,7 +42,7 @@ export const PropertiesPanel: React.FC = () => { } = useSDCPNContext(); const [panelWidth, setPanelWidthLocal] = useState( - DEFAULT_PROPERTIES_PANEL_WIDTH + DEFAULT_PROPERTIES_PANEL_WIDTH, ); // Sync panel width with global store @@ -51,7 +51,7 @@ export const PropertiesPanel: React.FC = () => { setPanelWidthLocal(newWidth); setPropertiesPanelWidth(newWidth); }, - [setPropertiesPanelWidth] + [setPropertiesPanelWidth], ); // Initialize store with starting width @@ -80,7 +80,7 @@ export const PropertiesPanel: React.FC = () => { switch (itemType) { case "place": { const placeData = petriNetDefinition.places.find( - (place) => place.id === selectedId + (place) => place.id === selectedId, ); if (placeData) { content = ( @@ -98,7 +98,7 @@ export const PropertiesPanel: React.FC = () => { case "transition": { const transitionData = petriNetDefinition.transitions.find( - (transition) => transition.id === selectedId + (transition) => transition.id === selectedId, ); if (transitionData) { content = ( @@ -117,7 +117,7 @@ export const PropertiesPanel: React.FC = () => { case "type": { const typeData = petriNetDefinition.types.find( - (type) => type.id === selectedId + (type) => type.id === selectedId, ); if (typeData) { content = ( @@ -133,7 +133,7 @@ export const PropertiesPanel: React.FC = () => { case "differentialEquation": { const equationData = petriNetDefinition.differentialEquations.find( - (equation) => equation.id === selectedId + (equation) => equation.id === selectedId, ); if (equationData) { content = ( @@ -151,7 +151,7 @@ export const PropertiesPanel: React.FC = () => { case "parameter": { const parameterData = petriNetDefinition.parameters.find( - (parameter) => parameter.id === selectedId + (parameter) => parameter.id === selectedId, ); if (parameterData) { content = ( @@ -183,9 +183,7 @@ export const PropertiesPanel: React.FC = () => { // Calculate bottom offset based on bottom panel visibility // Gap between PropertiesPanel and BottomPanel matches gap between LeftSideBar and BottomPanel - const bottomOffset = isBottomPanelOpen - ? bottomPanelHeight + PANEL_MARGIN - : 0; + const bottomOffset = isBottomPanelOpen ? bottomPanelHeight + PANEL_MARGIN : 0; return (
void + updateFn: (existingTransition: Transition) => void, ) => void; onArcWeightUpdate: ( transitionId: string, arcType: "input" | "output", placeId: string, - weight: number + weight: number, ) => void; } @@ -68,7 +68,7 @@ export const TransitionProperties: React.FC = ({ useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, - }) + }), ); const handleInputArcDragEnd = (event: DragEndEvent) => { @@ -76,17 +76,17 @@ export const TransitionProperties: React.FC = ({ if (over && active.id !== over.id) { const oldIndex = transition.inputArcs.findIndex( - (arc) => arc.placeId === active.id + (arc) => arc.placeId === active.id, ); const newIndex = transition.inputArcs.findIndex( - (arc) => arc.placeId === over.id + (arc) => arc.placeId === over.id, ); updateTransition(transition.id, (existingTransition) => { existingTransition.inputArcs = arrayMove( existingTransition.inputArcs, oldIndex, - newIndex + newIndex, ); }); } @@ -97,17 +97,17 @@ export const TransitionProperties: React.FC = ({ if (over && active.id !== over.id) { const oldIndex = transition.outputArcs.findIndex( - (arc) => arc.placeId === active.id + (arc) => arc.placeId === active.id, ); const newIndex = transition.outputArcs.findIndex( - (arc) => arc.placeId === over.id + (arc) => arc.placeId === over.id, ); updateTransition(transition.id, (existingTransition) => { existingTransition.outputArcs = arrayMove( existingTransition.outputArcs, oldIndex, - newIndex + newIndex, ); }); } @@ -116,7 +116,7 @@ export const TransitionProperties: React.FC = ({ const handleDeleteInputArc = (placeId: string) => { updateTransition(transition.id, (existingTransition) => { const index = existingTransition.inputArcs.findIndex( - (arc) => arc.placeId === placeId + (arc) => arc.placeId === placeId, ); if (index !== -1) { existingTransition.inputArcs.splice(index, 1); @@ -127,7 +127,7 @@ export const TransitionProperties: React.FC = ({ const handleDeleteOutputArc = (placeId: string) => { updateTransition(transition.id, (existingTransition) => { const index = existingTransition.outputArcs.findIndex( - (arc) => arc.placeId === placeId + (arc) => arc.placeId === placeId, ); if (index !== -1) { existingTransition.outputArcs.splice(index, 1); @@ -161,7 +161,7 @@ export const TransitionProperties: React.FC = ({ 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.` + `Are you sure you want to delete "${transition.name}"? All arcs connected to this transition will also be removed.`, ) ) { removeTransition(transition.id); @@ -244,7 +244,7 @@ export const TransitionProperties: React.FC = ({ > {transition.inputArcs.map((arc) => { const place = places.find( - (placeItem) => placeItem.id === arc.placeId + (placeItem) => placeItem.id === arc.placeId, ); return ( = ({ transition.id, "input", arc.placeId, - weight + weight, ); }} onDelete={() => handleDeleteInputArc(arc.placeId)} @@ -298,7 +298,7 @@ export const TransitionProperties: React.FC = ({ > {transition.outputArcs.map((arc) => { const place = places.find( - (placeItem) => placeItem.id === arc.placeId + (placeItem) => placeItem.id === arc.placeId, ); return ( = ({ transition.id, "output", arc.placeId, - weight + weight, ); }} onDelete={() => handleDeleteOutputArc(arc.placeId)} @@ -418,7 +418,7 @@ export const TransitionProperties: React.FC = ({ onClick: () => { updateTransition(transition.id, (existingTransition) => { existingTransition.lambdaCode = generateDefaultLambdaCode( - existingTransition.lambdaType + existingTransition.lambdaType, ); }); }, @@ -533,11 +533,11 @@ export const TransitionProperties: React.FC = ({ const inputs = transition.inputArcs .map((arc) => { const place = places.find( - (p) => p.id === arc.placeId + (p) => p.id === arc.placeId, ); if (!place || !place.colorId) return null; const type = types.find( - (t) => t.id === place.colorId + (t) => t.id === place.colorId, ); if (!type) return null; return { @@ -551,11 +551,11 @@ export const TransitionProperties: React.FC = ({ const outputs = transition.outputArcs .map((arc) => { const place = places.find( - (p) => p.id === arc.placeId + (p) => p.id === arc.placeId, ); if (!place || !place.colorId) return null; const type = types.find( - (t) => t.id === place.colorId + (t) => t.id === place.colorId, ); if (!type) return null; return { diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx index c556a7195ad..48cd563ebf1 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx @@ -101,13 +101,13 @@ export const TypeProperties: React.FC = ({ // Check for duplicates (excluding the current element) const isDuplicate = type.elements.some( - (elem) => elem.elementId !== elementId && elem.name === slugifiedName + (elem) => elem.elementId !== elementId && elem.name === slugifiedName, ); if (isDuplicate) { // eslint-disable-next-line no-alert alert( - `Warning: An element named "${slugifiedName}" already exists. Please use a unique name.` + `Warning: An element named "${slugifiedName}" already exists. Please use a unique name.`, ); return; } @@ -129,7 +129,7 @@ export const TypeProperties: React.FC = ({ // Confirmation dialog using browser API // eslint-disable-next-line no-alert const confirmed = window.confirm( - `Delete element "${elementName}"?\n\nThis cannot be undone.` + `Delete element "${elementName}"?\n\nThis cannot be undone.`, ); if (!confirmed) { @@ -138,7 +138,7 @@ export const TypeProperties: React.FC = ({ updateType(type.id, (existingType) => { const index = existingType.elements.findIndex( - (elem) => elem.elementId === elementId + (elem) => elem.elementId === elementId, ); if (index !== -1) { existingType.elements.splice(index, 1); @@ -320,8 +320,8 @@ export const TypeProperties: React.FC = ({ draggedIndex === index ? "rgba(59, 130, 246, 0.1)" : dragOverIndex === index - ? "rgba(59, 130, 246, 0.05)" - : "rgba(0, 0, 0, 0.03)", + ? "rgba(59, 130, 246, 0.05)" + : "rgba(0, 0, 0, 0.03)", borderRadius: 3, border: dragOverIndex === index @@ -393,13 +393,13 @@ export const TypeProperties: React.FC = ({ onChange={(event) => { handleUpdateElementName( element.elementId, - event.target.value + event.target.value, ); }} onBlur={(event) => { handleBlurElementName( element.elementId, - event.target.value + event.target.value, ); }} disabled={isDisabled} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx index c780e3d98b2..c4a47d47f7c 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/mode-selector.tsx @@ -101,7 +101,7 @@ export const ModeSelector: React.FC = ({ onValueChange={(details) => { if (details.value) { const selectedOption = options.find( - (opt) => opt.value === details.value + (opt) => opt.value === details.value, ); if (selectedOption && !selectedOption.disabled) { onChange(details.value as "edit" | "simulate"); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx index 858b9336c7c..440fc0b209c 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx @@ -46,7 +46,7 @@ export const EditorView = ({ // Get simulation store method to initialize parameter values const initializeParameterValuesFromDefaults = useSimulationStore( - (state) => state.initializeParameterValuesFromDefaults + (state) => state.initializeParameterValuesFromDefaults, ); // Handler for mode change that initializes parameter values when switching to simulate mode diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx index 95819c988c6..206ebc215e4 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/components/place-node.tsx @@ -106,14 +106,14 @@ export const PlaceNode: React.FC> = ({ selected, }: NodeProps) => { const isSimulateMode = useEditorStore( - (state) => state.globalMode === "simulate" + (state) => state.globalMode === "simulate", ); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId + (state) => state.selectedResourceId, ); const simulation = useSimulationStore((state) => state.simulation); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame + (state) => state.currentlyViewedFrame, ); const initialMarking = useSimulationStore((state) => state.initialMarking); @@ -144,8 +144,8 @@ export const PlaceNode: React.FC> = ({ const selectionVariant = isSelectedByResource ? "resource" : selected - ? "reactflow" - : "none"; + ? "reactflow" + : "none"; return (
diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index a63ccc59eb1..814f058c47b 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -66,10 +66,10 @@ export const SDCPNView: React.FC = () => { const setEditionMode = useEditorStore((state) => state.setEditionMode); const selectedItemIds = useEditorStore((state) => state.selectedItemIds); const setSelectedItemIds = useEditorStore( - (state) => state.setSelectedItemIds + (state) => state.setSelectedItemIds, ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId + (state) => state.setSelectedResourceId, ); const clearSelection = useEditorStore((state) => state.clearSelection); @@ -135,7 +135,7 @@ export const SDCPNView: React.FC = () => { // Shared function to create a node at a given position function createNodeAtPosition( nodeType: "place" | "transition", - position: { x: number; y: number } + position: { x: number; y: number }, ) { const { width, height } = nodeDimensions[nodeType]; From fc0baa70291498712d6e5bb45a327a0e70a928d1 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 16:58:10 +0100 Subject: [PATCH 37/40] Remove dependency of SimulationProvider on EditorContext and reorder Content order --- libs/@hashintel/petrinaut/src/petrinaut.tsx | 17 ++++++-------- .../src/state/simulation-provider.tsx | 22 ++----------------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/petrinaut.tsx b/libs/@hashintel/petrinaut/src/petrinaut.tsx index a3dde1c3aae..daabadc41e5 100644 --- a/libs/@hashintel/petrinaut/src/petrinaut.tsx +++ b/libs/@hashintel/petrinaut/src/petrinaut.tsx @@ -46,10 +46,7 @@ export type PetrinautProps = { /** * Create a new net and load it into the editor. */ - createNewNet: (params: { - petriNetDefinition: SDCPN; - title: string; - }) => void; + createNewNet: (params: { petriNetDefinition: SDCPN; title: string }) => void; /** * Whether to hide controls relating to net loading, creation and title. */ @@ -105,14 +102,14 @@ export const Petrinaut = ({ }: PetrinautProps) => { return ( - - - + + + - - - + + + ); }; diff --git a/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx b/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx index 93d66caaa31..fcb7e85e48a 100644 --- a/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx +++ b/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx @@ -1,7 +1,6 @@ import { createContext, useContext, useEffect, useRef } from "react"; import { useStore } from "zustand"; -import { EditorContext } from "./editor-provider"; import { useSDCPNContext } from "./sdcpn-provider"; import type { SimulationStoreState } from "./simulation-store"; import { createSimulationStore } from "./simulation-store"; @@ -18,12 +17,6 @@ export const SimulationProvider: React.FC = ({ const sdcpnContext = useSDCPNContext(); const { petriNetId } = sdcpnContext; - const editorStore = useContext(EditorContext); - - if (!editorStore) { - throw new Error("SimulationProvider must be used within EditorProvider"); - } - const sdcpnContextRef = useRef(sdcpnContext); useEffect(() => { sdcpnContextRef.current = sdcpnContext; @@ -36,17 +29,6 @@ export const SimulationProvider: React.FC = ({ const simulationStore = createSimulationStore(getSDCPN); - useEffect(() => { - const unsub = editorStore.subscribe((prevState, newState) => { - if (prevState.globalMode !== newState.globalMode) { - simulationStore.getState().__reinitialize(); - } - }); - return () => { - unsub(); - }; - }, [editorStore, simulationStore]); - useEffect(() => { if (petriNetId) { simulationStore.getState().__reinitialize(); @@ -61,13 +43,13 @@ export const SimulationProvider: React.FC = ({ }; export function useSimulationStore( - selector: (state: SimulationStoreState) => T, + selector: (state: SimulationStoreState) => T ): T { const store = useContext(SimulationContext); if (!store) { throw new Error( - "useSimulationStore must be used within SimulationProvider", + "useSimulationStore must be used within SimulationProvider" ); } From 0c3962dca36c6dc05c9898fca8b32888452b990b Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 19:00:49 +0100 Subject: [PATCH 38/40] Add useIsReadOnly hook --- .../src/state/simulation-provider.tsx | 4 ++-- .../petrinaut/src/state/use-is-read-only.ts | 23 +++++++++++++++++++ .../differential-equation-properties.tsx | 16 +++---------- .../PropertiesPanel/parameter-properties.tsx | 11 ++------- .../PropertiesPanel/place-properties.tsx | 11 +++------ .../PropertiesPanel/properties-panel.tsx | 13 +---------- .../PropertiesPanel/transition-properties.tsx | 11 ++------- .../PropertiesPanel/type-properties.tsx | 11 ++------- 8 files changed, 38 insertions(+), 62 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/state/use-is-read-only.ts diff --git a/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx b/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx index fcb7e85e48a..eac6c1bd1dc 100644 --- a/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx +++ b/libs/@hashintel/petrinaut/src/state/simulation-provider.tsx @@ -43,13 +43,13 @@ export const SimulationProvider: React.FC = ({ }; export function useSimulationStore( - selector: (state: SimulationStoreState) => T + selector: (state: SimulationStoreState) => T, ): T { const store = useContext(SimulationContext); if (!store) { throw new Error( - "useSimulationStore must be used within SimulationProvider" + "useSimulationStore must be used within SimulationProvider", ); } diff --git a/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts new file mode 100644 index 00000000000..46686443709 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts @@ -0,0 +1,23 @@ +import { useEditorStore } from "./editor-provider"; +import { useSimulationStore } from "./simulation-provider"; + +/** + * Hook that determines if the editor is in read-only mode. + * + * 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) + * + * When read-only, structural changes to the SDCPN (places, transitions, arcs, etc.) + * are prevented to maintain consistency with the running simulation. + */ +export const useIsReadOnly = (): boolean => { + const globalMode = useEditorStore((state) => state.globalMode); + const simulationState = useSimulationStore((state) => state.state); + + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + + const isReadOnly = globalMode === "simulate" || isSimulationActive; + return isReadOnly; +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx index 0f3f02fa1db..497eca6d920 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx @@ -16,13 +16,12 @@ import type { DifferentialEquation, Place, } from "../../../../core/types/sdcpn"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import { useIsReadOnly } from "../../../../state/use-is-read-only"; interface DifferentialEquationPropertiesProps { differentialEquation: DifferentialEquation; types: Color[]; places: Place[]; - globalMode: "edit" | "simulate"; updateDifferentialEquation: ( equationId: string, updateFn: (equation: DifferentialEquation) => void, @@ -31,21 +30,12 @@ interface DifferentialEquationPropertiesProps { export const DifferentialEquationProperties: React.FC< DifferentialEquationPropertiesProps -> = ({ - differentialEquation, - types, - places, - globalMode, - updateDifferentialEquation, -}) => { +> = ({ differentialEquation, types, places, updateDifferentialEquation }) => { const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingTypeId, setPendingTypeId] = useState(null); const [showTypeDropdown, setShowTypeDropdown] = useState(false); - const simulationState = useSimulationStore((state) => state.state); - const isSimulationRunning = - simulationState === "Running" || simulationState === "Paused"; - const isReadOnly = globalMode === "simulate" || isSimulationRunning; + const isReadOnly = useIsReadOnly(); const associatedType = types.find( (type) => type.id === differentialEquation.colorId, diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx index eed4631959e..ae518f99e81 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx @@ -1,5 +1,5 @@ import type { Parameter } from "../../../../core/types/sdcpn"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import { useIsReadOnly } from "../../../../state/use-is-read-only"; /** * Slugifies a string to a valid JavaScript identifier. @@ -29,20 +29,13 @@ interface ParameterPropertiesProps { parameterId: string, updateFn: (parameter: Parameter) => void, ) => void; - globalMode: "edit" | "simulate"; } export const ParameterProperties: React.FC = ({ parameter, updateParameter, - globalMode, }) => { - const simulationState = useSimulationStore((state) => state.state); - - // Check if simulation is running or paused - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - const isDisabled = globalMode === "simulate" || isSimulationActive; + const isDisabled = useIsReadOnly(); const handleUpdateName = (event: React.ChangeEvent) => { updateParameter(parameter.id, (existingParameter) => { diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx index e80465afd75..6bf99e943ff 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx @@ -30,6 +30,7 @@ import { import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; +import { useIsReadOnly } from "../../../../state/use-is-read-only"; import { InitialStateEditor } from "./initial-state-editor"; import { VisualizerErrorBoundary } from "./visualizer-error-boundary"; @@ -37,7 +38,6 @@ interface PlacePropertiesProps { place: Place; types: Color[]; differentialEquations: DifferentialEquation[]; - globalMode: "edit" | "simulate"; updatePlace: (placeId: string, updateFn: (place: Place) => void) => void; } @@ -45,16 +45,11 @@ export const PlaceProperties: React.FC = ({ place, types, differentialEquations, - globalMode, updatePlace, }) => { const simulation = useSimulationStore((state) => state.simulation); - const simulationState = useSimulationStore((state) => state.state); - - // Check if simulation is running or paused - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - const isReadOnly = globalMode === "simulate" || isSimulationActive; + const isReadOnly = useIsReadOnly(); + const globalMode = useEditorStore((state) => state.globalMode); const initialMarking = useSimulationStore((state) => state.initialMarking); const setInitialMarking = useSimulationStore( (state) => state.setInitialMarking, diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx index 517042e3f69..cd9c201dda7 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx @@ -23,7 +23,6 @@ export const PropertiesPanel: React.FC = () => { const selectedResourceId = useEditorStore( (state) => state.selectedResourceId, ); - const globalMode = useEditorStore((state) => state.globalMode); const setPropertiesPanelWidth = useEditorStore( (state) => state.setPropertiesPanelWidth, ); @@ -88,7 +87,6 @@ export const PropertiesPanel: React.FC = () => { place={placeData} types={petriNetDefinition.types} differentialEquations={petriNetDefinition.differentialEquations} - globalMode={globalMode} updatePlace={updatePlace} /> ); @@ -106,7 +104,6 @@ export const PropertiesPanel: React.FC = () => { transition={transitionData} places={petriNetDefinition.places} types={petriNetDefinition.types} - globalMode={globalMode} onArcWeightUpdate={updateArcWeight} updateTransition={updateTransition} /> @@ -120,13 +117,7 @@ export const PropertiesPanel: React.FC = () => { (type) => type.id === selectedId, ); if (typeData) { - content = ( - - ); + content = ; } break; } @@ -141,7 +132,6 @@ export const PropertiesPanel: React.FC = () => { differentialEquation={equationData} types={petriNetDefinition.types} places={petriNetDefinition.places} - globalMode={globalMode} updateDifferentialEquation={updateDifferentialEquation} /> ); @@ -158,7 +148,6 @@ export const PropertiesPanel: React.FC = () => { ); } diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx index a3cd84e63b0..b58fe06c8d0 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx @@ -29,14 +29,13 @@ import { } from "../../../../core/default-codes"; import type { Color, Place, Transition } from "../../../../core/types/sdcpn"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import { useIsReadOnly } from "../../../../state/use-is-read-only"; import { SortableArcItem } from "./sortable-arc-item"; interface TransitionPropertiesProps { transition: Transition; places: Place[]; types: Color[]; - globalMode: "edit" | "simulate"; updateTransition: ( id: string, updateFn: (existingTransition: Transition) => void, @@ -53,16 +52,10 @@ export const TransitionProperties: React.FC = ({ transition, places, types, - globalMode, updateTransition, onArcWeightUpdate, }) => { - const simulationState = useSimulationStore((state) => state.state); - - // Check if simulation is running or paused - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - const isReadOnly = globalMode === "simulate" || isSimulationActive; + const isReadOnly = useIsReadOnly(); const sensors = useSensors( useSensor(PointerSensor), diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx index 48cd563ebf1..3a504ae3c0e 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; import type { Color } from "../../../../core/types/sdcpn"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import { useIsReadOnly } from "../../../../state/use-is-read-only"; import { ColorSelect } from "./color-select"; /** @@ -40,20 +40,13 @@ const slugifyToIdentifier = (input: string): string => { interface TypePropertiesProps { type: Color; updateType: (typeId: string, updateFn: (type: Color) => void) => void; - globalMode: "edit" | "simulate"; } export const TypeProperties: React.FC = ({ type, updateType, - globalMode, }) => { - const simulationState = useSimulationStore((state) => state.state); - - // Check if simulation is running or paused - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - const isDisabled = globalMode === "simulate" || isSimulationActive; + const isDisabled = useIsReadOnly(); const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); From 5f7fc61b592e61c10fce9c811b26c5ecbf040d2b Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 19:13:36 +0100 Subject: [PATCH 39/40] Review: remove comment --- libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index 814f058c47b..9948dac6b0b 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -73,7 +73,6 @@ export const SDCPNView: React.FC = () => { ); const clearSelection = useEditorStore((state) => state.clearSelection); - // Simulation state const simulationState = useSimulationStore((state) => state.state); // Center viewport on SDCPN load From 96c2265e2bfb35ac1c4bf6523234c2352dde156a Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 23:22:54 +0100 Subject: [PATCH 40/40] Remove globalMode check from TransitionProperties --- .../PropertiesPanel/transition-properties.tsx | 313 +++++++++--------- 1 file changed, 150 insertions(+), 163 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx index b58fe06c8d0..120a3bba6bb 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx @@ -38,13 +38,13 @@ interface TransitionPropertiesProps { types: Color[]; updateTransition: ( id: string, - updateFn: (existingTransition: Transition) => void, + updateFn: (existingTransition: Transition) => void ) => void; onArcWeightUpdate: ( transitionId: string, arcType: "input" | "output", placeId: string, - weight: number, + weight: number ) => void; } @@ -61,7 +61,7 @@ export const TransitionProperties: React.FC = ({ useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, - }), + }) ); const handleInputArcDragEnd = (event: DragEndEvent) => { @@ -69,17 +69,17 @@ export const TransitionProperties: React.FC = ({ if (over && active.id !== over.id) { const oldIndex = transition.inputArcs.findIndex( - (arc) => arc.placeId === active.id, + (arc) => arc.placeId === active.id ); const newIndex = transition.inputArcs.findIndex( - (arc) => arc.placeId === over.id, + (arc) => arc.placeId === over.id ); updateTransition(transition.id, (existingTransition) => { existingTransition.inputArcs = arrayMove( existingTransition.inputArcs, oldIndex, - newIndex, + newIndex ); }); } @@ -90,17 +90,17 @@ export const TransitionProperties: React.FC = ({ if (over && active.id !== over.id) { const oldIndex = transition.outputArcs.findIndex( - (arc) => arc.placeId === active.id, + (arc) => arc.placeId === active.id ); const newIndex = transition.outputArcs.findIndex( - (arc) => arc.placeId === over.id, + (arc) => arc.placeId === over.id ); updateTransition(transition.id, (existingTransition) => { existingTransition.outputArcs = arrayMove( existingTransition.outputArcs, oldIndex, - newIndex, + newIndex ); }); } @@ -109,7 +109,7 @@ export const TransitionProperties: React.FC = ({ const handleDeleteInputArc = (placeId: string) => { updateTransition(transition.id, (existingTransition) => { const index = existingTransition.inputArcs.findIndex( - (arc) => arc.placeId === placeId, + (arc) => arc.placeId === placeId ); if (index !== -1) { existingTransition.inputArcs.splice(index, 1); @@ -120,7 +120,7 @@ export const TransitionProperties: React.FC = ({ const handleDeleteOutputArc = (placeId: string) => { updateTransition(transition.id, (existingTransition) => { const index = existingTransition.outputArcs.findIndex( - (arc) => arc.placeId === placeId, + (arc) => arc.placeId === placeId ); if (index !== -1) { existingTransition.outputArcs.splice(index, 1); @@ -154,7 +154,7 @@ export const TransitionProperties: React.FC = ({ 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.`, + `Are you sure you want to delete "${transition.name}"? All arcs connected to this transition will also be removed.` ) ) { removeTransition(transition.id); @@ -237,7 +237,7 @@ export const TransitionProperties: React.FC = ({ > {transition.inputArcs.map((arc) => { const place = places.find( - (placeItem) => placeItem.id === arc.placeId, + (placeItem) => placeItem.id === arc.placeId ); return ( = ({ transition.id, "input", arc.placeId, - weight, + weight ); }} onDelete={() => handleDeleteInputArc(arc.placeId)} @@ -291,7 +291,7 @@ export const TransitionProperties: React.FC = ({ > {transition.outputArcs.map((arc) => { const place = places.find( - (placeItem) => placeItem.id === arc.placeId, + (placeItem) => placeItem.id === arc.placeId ); return ( = ({ transition.id, "output", arc.placeId, - weight, + weight ); }} onDelete={() => handleDeleteOutputArc(arc.placeId)} @@ -336,13 +336,11 @@ export const TransitionProperties: React.FC = ({ { value: "stochastic", label: "Stochastic Rate" }, ]} onChange={(value) => { - if (globalMode !== "simulate") { - updateTransition(transition.id, (existingTransition) => { - existingTransition.lambdaType = value as - | "predicate" - | "stochastic"; - }); - } + updateTransition(transition.id, (existingTransition) => { + existingTransition.lambdaType = value as + | "predicate" + | "stochastic"; + }); }} />
@@ -385,61 +383,60 @@ export const TransitionProperties: React.FC = ({ ? "Predicate Firing Code" : "Stochastic Firing Rate Code"}
- {globalMode === "edit" && ( - - - - } - items={[ - { - id: "load-default", - label: "Load default template", - onClick: () => { - updateTransition(transition.id, (existingTransition) => { - existingTransition.lambdaCode = generateDefaultLambdaCode( - existingTransition.lambdaType, - ); - }); - }, + + + + + } + items={[ + { + id: "load-default", + label: "Load default template", + onClick: () => { + updateTransition(transition.id, (existingTransition) => { + existingTransition.lambdaCode = generateDefaultLambdaCode( + existingTransition.lambdaType + ); + }); }, - { - id: "generate-ai", - label: ( - -
- - Generate with AI -
-
- ), - disabled: true, - onClick: () => { - // TODO: Implement AI generation - }, + }, + { + id: "generate-ai", + label: ( + +
+ + Generate with AI +
+
+ ), + disabled: true, + onClick: () => { + // TODO: Implement AI generation }, - ]} - /> - )} + }, + ]} + />
= ({ Transition Results
- {globalMode === "edit" && ( - - - - } - items={[ - { - id: "load-default", - label: "Load default template", - onClick: () => { - // Build input and output arc information for code generation - const inputs = transition.inputArcs - .map((arc) => { - const place = places.find( - (p) => p.id === arc.placeId, - ); - if (!place || !place.colorId) return null; - const type = types.find( - (t) => t.id === place.colorId, - ); - if (!type) return null; - return { - placeName: place.name, - type, - weight: arc.weight, - }; - }) - .filter((i) => i !== null); + + + + } + items={[ + { + id: "load-default", + label: "Load default template", + onClick: () => { + // Build input and output arc information for code generation + const inputs = transition.inputArcs + .map((arc) => { + const place = places.find((p) => p.id === arc.placeId); + if (!place || !place.colorId) return null; + const type = types.find((t) => t.id === place.colorId); + if (!type) return null; + return { + placeName: place.name, + type, + weight: arc.weight, + }; + }) + .filter((i) => i !== null); - const outputs = transition.outputArcs - .map((arc) => { - const place = places.find( - (p) => p.id === arc.placeId, - ); - if (!place || !place.colorId) return null; - const type = types.find( - (t) => t.id === place.colorId, - ); - if (!type) return null; - return { - placeName: place.name, - type, - weight: arc.weight, - }; - }) - .filter((o) => o !== null); + const outputs = transition.outputArcs + .map((arc) => { + const place = places.find((p) => p.id === arc.placeId); + if (!place || !place.colorId) return null; + const type = types.find((t) => t.id === place.colorId); + if (!type) return null; + return { + placeName: place.name, + type, + weight: arc.weight, + }; + }) + .filter((o) => o !== null); - updateTransition(transition.id, (existingTransition) => { - existingTransition.transitionKernelCode = - generateDefaultTransitionKernelCode(inputs, outputs); - }); - }, + updateTransition(transition.id, (existingTransition) => { + existingTransition.transitionKernelCode = + generateDefaultTransitionKernelCode(inputs, outputs); + }); }, - { - id: "generate-ai", - label: ( - -
- - Generate with AI -
-
- ), - disabled: true, - onClick: () => { - // TODO: Implement AI generation - }, + }, + { + id: "generate-ai", + label: ( + +
+ + Generate with AI +
+
+ ), + disabled: true, + onClick: () => { + // TODO: Implement AI generation }, - ]} - /> - )} + }, + ]} + />