From 1b62f3798fe749b64e7a2cd0c1617f8325beedd1 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Sat, 20 Dec 2025 13:54:22 +0100 Subject: [PATCH 1/5] Add Panel SubView, with Horizontal and Vertical SubViews containers --- .../sub-view/horizontal-tabs-container.tsx | 168 ++++++++++ .../src/components/sub-view/index.ts | 8 + .../src/components/sub-view/types.ts | 30 ++ .../sub-view/vertical-sub-views-container.tsx | 161 +++++++++ .../components/BottomPanel/bottom-panel.tsx | 116 ++----- .../BottomPanel/diagnostics-content.tsx | 11 + .../BottomPanel/parameters-content.tsx | 36 +- .../simulation-settings-content.tsx | 12 + .../differential-equations-section.tsx | 296 ++++++++-------- .../components/LeftSideBar/left-sidebar.tsx | 36 +- .../components/LeftSideBar/nodes-section.tsx | 181 ++++------ .../components/LeftSideBar/types-section.tsx | 316 ++++++++---------- 12 files changed, 815 insertions(+), 556 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx create mode 100644 libs/@hashintel/petrinaut/src/components/sub-view/index.ts create mode 100644 libs/@hashintel/petrinaut/src/components/sub-view/types.ts create mode 100644 libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx b/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx new file mode 100644 index 00000000000..c7a4440f55b --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx @@ -0,0 +1,168 @@ +import { css, cva } from "@hashintel/ds-helpers/css"; + +import { InfoIconTooltip } from "../tooltip"; +import type { SubView } from "./types"; + +const tabsContainerStyle = css({ + display: "flex", + alignItems: "center", + gap: "[4px]", +}); + +const tabButtonStyle = cva({ + base: { + fontSize: "[11px]", + fontWeight: "[500]", + padding: "[4px 10px]", + textTransform: "uppercase", + borderRadius: "[3px]", + border: "none", + cursor: "pointer", + 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", + }, + }, + }, + }, +}); + +const contentStyle = css({ + fontSize: "[12px]", + padding: "[12px 12px]", + flex: "[1]", + overflowY: "auto", +}); + +interface TabButtonProps { + subView: SubView; + isActive: boolean; + onClick: () => void; +} + +const TabButton: React.FC = ({ subView, isActive, onClick }) => { + return ( + + ); +}; + +interface HorizontalTabsContainerProps { + /** Array of subviews to display as tabs */ + subViews: SubView[]; + /** ID of the currently active tab */ + activeTabId: string; + /** Callback when a tab is selected */ + onTabChange: (tabId: string) => void; +} + +/** + * Container that displays subviews as horizontal tabs. + * Used in the BottomPanel for Diagnostics, Parameters, and Simulation Settings. + * + * This component returns both the tabs header and the content area as separate + * parts that can be composed into the parent layout. + */ +export const HorizontalTabsContainer: React.FC = ({ + subViews, + activeTabId, + onTabChange, +}) => { + const activeSubView = subViews.find((sv) => sv.id === activeTabId) ?? subViews[0]; + + if (!activeSubView) { + return null; + } + + const Component = activeSubView.component; + + return ( + <> + {/* Tab Header */} +
+ {subViews.map((subView) => ( + onTabChange(subView.id)} + /> + ))} +
+ + {/* Content */} +
+ +
+ + ); +}; + +/** + * Renders just the tab bar portion of the horizontal tabs. + * Useful when you need to compose the tabs header separately from the content. + */ +export const HorizontalTabsHeader: React.FC<{ + subViews: SubView[]; + activeTabId: string; + onTabChange: (tabId: string) => void; +}> = ({ subViews, activeTabId, onTabChange }) => { + return ( +
+ {subViews.map((subView) => ( + onTabChange(subView.id)} + /> + ))} +
+ ); +}; + +/** + * Renders just the content portion of the horizontal tabs. + * Useful when you need to compose the content separately from the tabs header. + */ +export const HorizontalTabsContent: React.FC<{ + subViews: SubView[]; + activeTabId: string; +}> = ({ subViews, activeTabId }) => { + const activeSubView = subViews.find((sv) => sv.id === activeTabId) ?? subViews[0]; + + if (!activeSubView) { + return null; + } + + const Component = activeSubView.component; + + return ( +
+ +
+ ); +}; + diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/index.ts b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts new file mode 100644 index 00000000000..451ff0fbf99 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts @@ -0,0 +1,8 @@ +export type { SubView } from "./types"; +export { VerticalSubViewsContainer } from "./vertical-sub-views-container"; +export { + HorizontalTabsContainer, + HorizontalTabsHeader, + HorizontalTabsContent, +} from "./horizontal-tabs-container"; + diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/types.ts b/libs/@hashintel/petrinaut/src/components/sub-view/types.ts new file mode 100644 index 00000000000..3367800a192 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/sub-view/types.ts @@ -0,0 +1,30 @@ +import type { ComponentType, ReactNode } from "react"; + +/** + * SubView represents a single view that can be displayed in either: + * - A vertical collapsible section (LeftSideBar) + * - A horizontal tab (BottomPanel) + * + * This abstraction allows views to be easily moved between panels. + */ +export interface SubView { + /** Unique identifier for the subview */ + id: string; + /** Title displayed in the section header or tab */ + title: string; + /** Optional tooltip shown when hovering over the title/tab */ + tooltip?: string; + /** The component to render for this subview */ + component: ComponentType; + /** + * Optional render function for the header right side (e.g., add button). + * Only used in vertical (collapsible) layout. + */ + renderHeaderAction?: () => ReactNode; + /** + * Whether this subview should grow to fill available space. + * Only affects vertical layout. Defaults to false. + */ + flexGrow?: boolean; +} + diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx b/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx new file mode 100644 index 00000000000..b2a580142da --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx @@ -0,0 +1,161 @@ +import { css, cva } from "@hashintel/ds-helpers/css"; +import { useState } from "react"; +import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; + +import { InfoIconTooltip } from "../tooltip"; +import type { SubView } from "./types"; + +const sectionContainerStyle = cva({ + base: { + display: "flex", + flexDirection: "column", + gap: "[8px]", + }, + variants: { + flexGrow: { + true: { + flex: "[1]", + minHeight: "[0]", + }, + false: { + paddingBottom: "[16px]", + }, + }, + }, +}); + +const headerRowStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", +}); + +const sectionToggleButtonStyle = css({ + display: "flex", + alignItems: "center", + gap: "[6px]", + fontWeight: 600, + fontSize: "[13px]", + color: "[#333]", + cursor: "pointer", + background: "[transparent]", + border: "none", + padding: "spacing.1", + borderRadius: "radius.4", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + }, +}); + +const contentContainerStyle = cva({ + base: { + display: "flex", + flexDirection: "column", + gap: "[2px]", + overflowY: "auto", + }, + variants: { + flexGrow: { + true: { + flex: "[1]", + }, + false: { + maxHeight: "[200px]", + }, + }, + }, +}); + +interface VerticalSubViewSectionProps { + subView: SubView; + defaultExpanded?: boolean; + isLast?: boolean; +} + +/** + * A single collapsible section within the vertical container. + */ +const VerticalSubViewSection: React.FC = ({ + subView, + defaultExpanded = true, + isLast = false, +}) => { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const { + id, + title, + tooltip, + component: Component, + renderHeaderAction, + flexGrow = false, + } = subView; + + // Last item should grow if it has flexGrow, but shouldn't have bottom border + const effectiveFlexGrow = isLast ? flexGrow : false; + + return ( +
+
+ + {renderHeaderAction?.()} +
+ + {isExpanded && ( +
+ +
+ )} +
+ ); +}; + +interface VerticalSubViewsContainerProps { + /** Array of subviews to display as collapsible sections */ + subViews: SubView[]; + /** Whether sections should be expanded by default */ + defaultExpanded?: boolean; +} + +/** + * Container that displays subviews as vertically stacked collapsible sections. + * Used in the LeftSideBar for Token Types, Differential Equations, and Nodes. + */ +export const VerticalSubViewsContainer: React.FC< + VerticalSubViewsContainerProps +> = ({ subViews, defaultExpanded = true }) => { + return ( + <> + {subViews.map((subView, index) => ( + + ))} + + ); +}; 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 fcc99ce0708..430b20ab950 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,7 +1,13 @@ -import { css, cva } from "@hashintel/ds-helpers/css"; +import { css } from "@hashintel/ds-helpers/css"; +import { useCallback } from "react"; import { FaXmark } from "react-icons/fa6"; import { GlassPanel } from "../../../../components/glass-panel"; +import { + HorizontalTabsContent, + HorizontalTabsHeader, +} from "../../../../components/sub-view"; +import type { SubView } from "../../../../components/sub-view/types"; import { MAX_BOTTOM_PANEL_HEIGHT, MIN_BOTTOM_PANEL_HEIGHT, @@ -9,9 +15,17 @@ import { } 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"; +import { diagnosticsSubView } from "./diagnostics-content"; +import { parametersSubView } from "./parameters-content"; +import { simulationSettingsSubView } from "./simulation-settings-content"; + +// Pre-defined array of subviews for the bottom panel +// Note: Using explicit array typing due to TypeScript inference quirks with barrel imports +const BOTTOM_PANEL_SUBVIEWS: SubView[] = [ + diagnosticsSubView, + parametersSubView, + simulationSettingsSubView, +]; const glassPanelBaseStyle = css({ position: "fixed", @@ -32,12 +46,6 @@ const headerStyle = css({ flexShrink: 0, }); -const tabsContainerStyle = css({ - display: "flex", - alignItems: "center", - gap: "[4px]", -}); - const closeButtonStyle = css({ display: "flex", alignItems: "center", @@ -56,51 +64,6 @@ const closeButtonStyle = css({ }, }); -const tabButtonStyle = cva({ - base: { - fontSize: "[11px]", - fontWeight: "[500]", - padding: "[4px 10px]", - textTransform: "uppercase", - borderRadius: "[3px]", - border: "none", - cursor: "pointer", - 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", - }, - }, - }, - }, -}); - -const contentStyle = css({ - fontSize: "[12px]", - padding: "[12px 12px]", - 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. @@ -113,12 +76,23 @@ 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); const toggleBottomPanel = useEditorStore((state) => state.toggleBottomPanel); + // Use the pre-defined array of subviews + const subViews = BOTTOM_PANEL_SUBVIEWS; + + // Handler for tab change that casts string to BottomPanelTab + const handleTabChange = useCallback( + (tabId: string) => { + setActiveTab(tabId as BottomPanelTab); + }, + [setActiveTab] + ); + if (!isOpen) { return null; } @@ -129,17 +103,6 @@ export const BottomPanel: React.FC = () => { ? leftSidebarWidth + PANEL_MARGIN * 2 : PANEL_MARGIN; - function renderContent() { - switch (activeTab) { - case "diagnostics": - return ; - case "simulation-settings": - return ; - case "parameters": - return ; - } - } - return ( { > {/* Tab Header */}
-
- {tabs.map((tab) => ( - - ))} -
+
{/* 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 index 4a430254af6..75c3870f5e6 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 @@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from "react"; import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; import ts from "typescript"; +import type { SubView } from "../../../../components/sub-view/types"; import { useCheckerContext } from "../../../../state/checker-provider"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; @@ -290,3 +291,13 @@ export const DiagnosticsContent: React.FC = () => { ); }; + +/** + * SubView definition for Diagnostics tab. + */ +export const diagnosticsSubView: SubView = { + id: "diagnostics", + title: "Diagnostics", + tooltip: "View compilation errors and warnings in your model code.", + component: DiagnosticsContent, +}; 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 c8cced8093c..16f161f4412 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 @@ -1,6 +1,7 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { v4 as uuidv4 } from "uuid"; +import type { SubView } from "../../../../components/sub-view/types"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; @@ -8,17 +9,8 @@ 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]", + justifyContent: "flex-end", + marginBottom: "[8px]", }); const addButtonStyle = css({ @@ -189,11 +181,8 @@ export const ParametersContent: React.FC = () => { return (
-
-
- Parameters are injected into dynamics, lambda, and kernel functions. -
- {!isSimulationMode && ( + {!isSimulationMode && ( +
- )} -
+
+ )}
{parameters.map((param) => { @@ -282,3 +271,14 @@ export const ParametersContent: React.FC = () => {
); }; + +/** + * SubView definition for Global Parameters tab. + */ +export const parametersSubView: SubView = { + id: "parameters", + title: "Global Parameters", + tooltip: + "Parameters are injected into dynamics, lambda, and kernel functions.", + component: ParametersContent, +}; 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 0bc779161cc..bfe309fd396 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 type { SubView } from "../../../../components/sub-view/types"; import { InfoIconTooltip } from "../../../../components/tooltip"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; @@ -325,3 +326,14 @@ export const SimulationSettingsContent: React.FC = () => {
); }; + +/** + * SubView definition for Simulation Settings tab. + */ +export const simulationSettingsSubView: SubView = { + id: "simulation-settings", + title: "Simulation Settings", + tooltip: + "Configure simulation parameters including time step and ODE solver method.", + component: SimulationSettingsContent, +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx index e9f2ba9f846..d29ce47b70d 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx @@ -1,72 +1,12 @@ import { css, cva } 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 type { SubView } from "../../../../components/sub-view/types"; import { DEFAULT_DIFFERENTIAL_EQUATION_CODE } from "../../../../core/default-codes"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; -const sectionContainerStyle = css({ - display: "flex", - flexDirection: "column", - gap: "[8px]", - paddingBottom: "[16px]", - borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", -}); - -const headerRowStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", -}); - -const sectionToggleButtonStyle = css({ - display: "flex", - alignItems: "center", - gap: "[6px]", - fontWeight: 600, - fontSize: "[13px]", - color: "[#333]", - cursor: "pointer", - background: "[transparent]", - border: "none", - padding: "spacing.1", - borderRadius: "radius.4", - _hover: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - }, -}); - -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", - }, - _disabled: { - cursor: "not-allowed", - opacity: "[0.4]", - _hover: { - backgroundColor: "[transparent]", - color: "core.gray.60", - }, - }, -}); - const listContainerStyle = css({ display: "flex", flexDirection: "column", @@ -141,12 +81,40 @@ const emptyMessageStyle = css({ textAlign: "center", }); -export const DifferentialEquationsSection: React.FC = () => { - const [isExpanded, setIsExpanded] = useState(true); +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", + }, + _disabled: { + cursor: "not-allowed", + opacity: "[0.4]", + _hover: { + backgroundColor: "[transparent]", + color: "core.gray.60", + }, + }, +}); +/** + * DifferentialEquationsSectionContent displays the list of differential equations. + * This is the content portion without the collapsible header. + */ +export const DifferentialEquationsSectionContent: React.FC = () => { const { - petriNetDefinition: { types, differentialEquations }, - addDifferentialEquation, + petriNetDefinition: { differentialEquations }, removeDifferentialEquation, } = useSDCPNContext(); @@ -163,103 +131,111 @@ export const DifferentialEquationsSection: React.FC = () => { simulationState === "Running" || simulationState === "Paused"; return ( -
-
- - -
- {isExpanded && ( -
- {differentialEquations.map((eq) => { - const isSelected = selectedResourceId === eq.id; - - return ( -
{ - // Don't trigger selection if clicking the delete button - if ( - event.target instanceof HTMLElement && - event.target.closest("button[aria-label^='Delete']") - ) { - return; - } - setSelectedResourceId(eq.id); - }} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - setSelectedResourceId(eq.id); - } - }} - className={equationRowStyle({ isSelected })} - > -
- {eq.name} -
- -
- ); - })} - {differentialEquations.length === 0 && ( -
- No differential equations yet +
+ {differentialEquations.map((eq) => { + const isSelected = selectedResourceId === eq.id; + + return ( +
{ + // Don't trigger selection if clicking the delete button + if ( + event.target instanceof HTMLElement && + event.target.closest("button[aria-label^='Delete']") + ) { + return; + } + setSelectedResourceId(eq.id); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + setSelectedResourceId(eq.id); + } + }} + className={equationRowStyle({ isSelected })} + > +
+ {eq.name}
- )} -
+ +
+ ); + })} + {differentialEquations.length === 0 && ( +
No differential equations yet
)}
); }; + +/** + * DifferentialEquationsSectionHeaderAction renders the add button for the section header. + */ +export const DifferentialEquationsSectionHeaderAction: React.FC = () => { + const { + petriNetDefinition: { types, differentialEquations }, + addDifferentialEquation, + } = useSDCPNContext(); + const setSelectedResourceId = useEditorStore( + (state) => state.setSelectedResourceId, + ); + + // Check if simulation is running or paused + const simulationState = useSimulationStore((state) => state.state); + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + + return ( + + ); +}; + +/** + * SubView definition for Differential Equations section. + */ +export const differentialEquationsSectionSubView: SubView = { + id: "differential-equations", + title: "Differential Equations", + tooltip: `Differential equations govern how token data changes over time when tokens remain in a place ("dynamics").`, + component: DifferentialEquationsSectionContent, + renderHeaderAction: () => , + flexGrow: false, +}; 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 6986fa1b3ce..1bd5eed5efa 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,5 @@ import { css, cva } from "@hashintel/ds-helpers/css"; +import { useMemo } from "react"; import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarRightCollapse, @@ -6,17 +7,19 @@ import { import { GlassPanel } from "../../../../components/glass-panel"; import type { MenuItem } from "../../../../components/menu"; +import { VerticalSubViewsContainer } from "../../../../components/sub-view"; +import type { SubView } from "../../../../components/sub-view/types"; 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 { differentialEquationsSectionSubView } from "./differential-equations-section"; import { FloatingTitle } from "./floating-title"; import { HamburgerMenu } from "./hamburger-menu"; -import { NodesSection } from "./nodes-section"; -import { TypesSection } from "./types-section"; +import { nodesSectionSubView } from "./nodes-section"; +import { typesSectionSubView } from "./types-section"; const outerContainerStyle = cva({ base: { @@ -148,11 +151,21 @@ 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 + ); + + // Define the subviews for the left sidebar + const subViews: SubView[] = useMemo( + () => [ + typesSectionSubView, + differentialEquationsSectionSubView, + nodesSectionSubView, + ], + [] ); return ( @@ -207,18 +220,7 @@ export const LeftSideBar: React.FC = ({
{/* Content sections - only visible when open */} - {isOpen && ( - <> - {/* Types Section - only in Edit mode */} - - - {/* Differential Equations Section - only in Edit mode */} - - - {/* Nodes Section */} - - - )} + {isOpen && }
); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx index 57446169287..e2bc691b497 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx @@ -1,47 +1,14 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; -import { - FaChevronDown, - FaChevronRight, - FaCircle, - FaSquare, -} from "react-icons/fa6"; +import { FaCircle, FaSquare } from "react-icons/fa6"; -import { InfoIconTooltip } from "../../../../components/tooltip"; +import type { SubView } from "../../../../components/sub-view/types"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -const sectionContainerStyle = css({ - display: "flex", - flexDirection: "column", - gap: "[8px]", - flex: "[1]", - minHeight: "[0]", -}); - -const sectionToggleButtonStyle = css({ - display: "flex", - alignItems: "center", - gap: "[6px]", - fontWeight: 600, - fontSize: "[13px]", - color: "[#333]", - cursor: "pointer", - background: "[transparent]", - border: "none", - padding: "spacing.1", - borderRadius: "radius.4", - _hover: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - }, -}); - const listContainerStyle = css({ display: "flex", flexDirection: "column", gap: "[2px]", - overflowY: "auto", - flex: "[1]", }); const nodeRowStyle = cva({ @@ -116,8 +83,11 @@ const emptyMessageStyle = css({ textAlign: "center", }); -export const NodesSection: React.FC = () => { - const [isExpanded, setIsExpanded] = useState(true); +/** + * NodesSectionContent displays the list of places and transitions. + * This is the content portion without the collapsible header. + */ +export const NodesSectionContent: React.FC = () => { const { petriNetDefinition: { places, transitions }, } = useSDCPNContext(); @@ -134,82 +104,73 @@ export const NodesSection: React.FC = () => { }; return ( -
- +
+ {/* Places */} + {places.map((place) => { + const isSelected = selectedResourceId === place.id; + return ( +
handleLayerClick(place.id)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleLayerClick(place.id); + } + }} + className={nodeRowStyle({ isSelected })} + > + + + {place.name || `Place ${place.id}`} + +
+ ); + })} - {/* Nodes List */} - {isExpanded && ( -
- {/* Places */} - {places.map((place) => { - const isSelected = selectedResourceId === place.id; - return ( -
handleLayerClick(place.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - handleLayerClick(place.id); - } - }} - className={nodeRowStyle({ isSelected })} - > - - - {place.name || `Place ${place.id}`} - -
- ); - })} + {/* Transitions */} + {transitions.map((transition) => { + const isSelected = selectedResourceId === transition.id; + return ( +
handleLayerClick(transition.id)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleLayerClick(transition.id); + } + }} + className={nodeRowStyle({ isSelected })} + > + + + {transition.name || `Transition ${transition.id}`} + +
+ ); + })} - {/* Transitions */} - {transitions.map((transition) => { - const isSelected = selectedResourceId === transition.id; - return ( -
handleLayerClick(transition.id)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - handleLayerClick(transition.id); - } - }} - className={nodeRowStyle({ isSelected })} - > - - - {transition.name || `Transition ${transition.id}`} - -
- ); - })} - - {/* Empty state */} - {places.length === 0 && transitions.length === 0 && ( -
No nodes yet
- )} -
+ {/* Empty state */} + {places.length === 0 && transitions.length === 0 && ( +
No nodes yet
)}
); }; + +/** + * SubView definition for Nodes section. + */ +export const nodesSectionSubView: SubView = { + id: "nodes", + title: "Nodes", + tooltip: + "Manage nodes in the net, including places and transitions. Places represent states in the net, and transitions represent events which change the state of the net.", + component: NodesSectionContent, + flexGrow: true, +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx index 2cef5ab758f..4c53a717abb 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx @@ -1,76 +1,14 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; -import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; -import { InfoIconTooltip } from "../../../../components/tooltip"; +import type { SubView } from "../../../../components/sub-view/types"; import { useEditorStore } from "../../../../state/editor-provider"; import { useSDCPNContext } from "../../../../state/sdcpn-provider"; import { useSimulationStore } from "../../../../state/simulation-provider"; -const sectionContainerStyle = css({ - display: "flex", - flexDirection: "column", - gap: "[8px]", - paddingBottom: "[16px]", - borderBottom: "[1px solid rgba(0, 0, 0, 0.1)]", -}); - -const headerRowStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", -}); - -const sectionToggleButtonStyle = css({ - display: "flex", - alignItems: "center", - gap: "[6px]", - fontWeight: 600, - fontSize: "[13px]", - color: "[#333]", - cursor: "pointer", - background: "[transparent]", - border: "none", - padding: "spacing.1", - borderRadius: "radius.4", - _hover: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - }, -}); - -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", - }, - _disabled: { - cursor: "not-allowed", - opacity: "[0.4]", - _hover: { - backgroundColor: "[transparent]", - color: "core.gray.60", - }, - }, -}); - const listContainerStyle = css({ display: "flex", flexDirection: "column", gap: "[2px]", - maxHeight: "[200px]", - overflowY: "auto", }); const typeRowStyle = cva({ @@ -150,6 +88,33 @@ const emptyMessageStyle = css({ textAlign: "center", }); +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", + }, + _disabled: { + cursor: "not-allowed", + opacity: "[0.4]", + _hover: { + backgroundColor: "[transparent]", + color: "core.gray.60", + }, + }, +}); + // Pool of 10 well-differentiated colors for types const TYPE_COLOR_POOL = [ "#3b82f6", // Blue @@ -194,12 +159,13 @@ const getNextTypeNumber = (existingNames: string[]): number => { return maxNumber + 1; }; -export const TypesSection: React.FC = () => { - const [isExpanded, setIsExpanded] = useState(true); - +/** + * TypesSectionContent displays the list of token types. + * This is the content portion without the collapsible header. + */ +export const TypesSectionContent: React.FC = () => { const { petriNetDefinition: { types }, - addType, removeType, } = useSDCPNContext(); @@ -216,112 +182,120 @@ export const TypesSection: React.FC = () => { simulationState === "Running" || simulationState === "Paused"; return ( -
-
- - -
- - {isExpanded && ( -
- {types.map((type) => { - const isSelected = selectedResourceId === type.id; +
+ {types.map((type) => { + const isSelected = selectedResourceId === type.id; - return ( -
{ - // Don't trigger selection if clicking the delete button - if ( - event.target instanceof HTMLElement && - event.target.closest("button[aria-label^='Delete']") - ) { - return; - } - setSelectedResourceId(type.id); - }} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - setSelectedResourceId(type.id); - } - }} - className={typeRowStyle({ isSelected })} - > -
- {type.name} - -
- ); - })} - {types.length === 0 && ( -
No token types yet
- )} -
+ return ( +
{ + // Don't trigger selection if clicking the delete button + if ( + event.target instanceof HTMLElement && + event.target.closest("button[aria-label^='Delete']") + ) { + return; + } + setSelectedResourceId(type.id); + }} + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + setSelectedResourceId(type.id); + } + }} + className={typeRowStyle({ isSelected })} + > +
+ {type.name} + +
+ ); + })} + {types.length === 0 && ( +
No token types yet
)}
); }; + +/** + * TypesSectionHeaderAction renders the add button for the types section header. + */ +export const TypesSectionHeaderAction: React.FC = () => { + const { + petriNetDefinition: { types }, + addType, + } = useSDCPNContext(); + + // Check if simulation is running or paused + const simulationState = useSimulationStore((state) => state.state); + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + + return ( + + ); +}; + +/** + * SubView definition for Token Types section. + */ +export const typesSectionSubView: SubView = { + id: "token-types", + title: "Token Types", + tooltip: "Manage data types which can be assigned to tokens in a place.", + component: TypesSectionContent, + renderHeaderAction: () => , + flexGrow: false, +}; From 546e9ace8a5d5e4ae7dc75f81f3c2ba90a1e0002 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Sat, 20 Dec 2025 14:34:56 +0100 Subject: [PATCH 2/5] Make the BottomPanel display header actions --- .../sub-view/horizontal-tabs-container.tsx | 18 ++++ .../src/components/sub-view/index.ts | 1 + .../components/BottomPanel/bottom-panel.tsx | 29 ++++-- .../BottomPanel/parameters-content.tsx | 88 +++++++++++-------- 4 files changed, 92 insertions(+), 44 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx b/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx index c7a4440f55b..105c6128493 100644 --- a/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx +++ b/libs/@hashintel/petrinaut/src/components/sub-view/horizontal-tabs-container.tsx @@ -143,6 +143,24 @@ export const HorizontalTabsHeader: React.FC<{ ); }; +/** + * Returns the header action for the currently active tab. + * Used to render custom actions (like add buttons) in the panel header. + */ +export const HorizontalTabsHeaderAction: React.FC<{ + subViews: SubView[]; + activeTabId: string; +}> = ({ subViews, activeTabId }) => { + const activeSubView = + subViews.find((sv) => sv.id === activeTabId) ?? subViews[0]; + + if (!activeSubView?.renderHeaderAction) { + return null; + } + + return <>{activeSubView.renderHeaderAction()}; +}; + /** * Renders just the content portion of the horizontal tabs. * Useful when you need to compose the content separately from the tabs header. diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/index.ts b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts index 451ff0fbf99..804681cb1c9 100644 --- a/libs/@hashintel/petrinaut/src/components/sub-view/index.ts +++ b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts @@ -3,6 +3,7 @@ export { VerticalSubViewsContainer } from "./vertical-sub-views-container"; export { HorizontalTabsContainer, HorizontalTabsHeader, + HorizontalTabsHeaderAction, HorizontalTabsContent, } from "./horizontal-tabs-container"; 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 430b20ab950..f0797a2a7ea 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 @@ -6,6 +6,7 @@ import { GlassPanel } from "../../../../components/glass-panel"; import { HorizontalTabsContent, HorizontalTabsHeader, + HorizontalTabsHeaderAction, } from "../../../../components/sub-view"; import type { SubView } from "../../../../components/sub-view/types"; import { @@ -46,6 +47,12 @@ const headerStyle = css({ flexShrink: 0, }); +const headerRightStyle = css({ + display: "flex", + alignItems: "center", + gap: "[4px]", +}); + const closeButtonStyle = css({ display: "flex", alignItems: "center", @@ -128,14 +135,20 @@ export const BottomPanel: React.FC = () => { activeTabId={activeTab} onTabChange={handleTabChange} /> - +
+ + +
{/* Scrollable content */} 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 16f161f4412..9e04e425d32 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 @@ -6,13 +6,6 @@ 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: "flex-end", - marginBottom: "[8px]", -}); - const addButtonStyle = css({ display: "flex", alignItems: "center", @@ -137,32 +130,21 @@ const emptyMessageStyle = css({ }); /** - * ParametersContent displays global parameters in the BottomPanel. + * Header action component for adding parameters. + * Shown in the panel header when not in simulation mode. */ -export const ParametersContent: React.FC = () => { +const ParametersHeaderAction: 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, + (state) => state.setSelectedResourceId ); - const isSimulationNotRun = - globalMode === "simulate" && simulationState === "NotRun"; const isSimulationMode = globalMode === "simulate"; - - // Check if simulation is running or paused const isSimulationActive = simulationState === "Running" || simulationState === "Paused"; @@ -179,22 +161,55 @@ export const ParametersContent: React.FC = () => { setSelectedResourceId(id); }; + // Don't show add button in simulation mode + if (isSimulationMode) { + return null; + } + return ( -
- {!isSimulationMode && ( -
- -
- )} + + ); +}; + +/** + * ParametersContent displays global parameters in the BottomPanel. + */ +export const ParametersContent: React.FC = () => { + const { + petriNetDefinition: { parameters }, + 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"; + // Check if simulation is running or paused + const isSimulationActive = + simulationState === "Running" || simulationState === "Paused"; + + return ( +
{parameters.map((param) => { const isSelected = selectedResourceId === param.id; @@ -281,4 +296,5 @@ export const parametersSubView: SubView = { tooltip: "Parameters are injected into dynamics, lambda, and kernel functions.", component: ParametersContent, + renderHeaderAction: () => , }; From 5cdd304d94f88e93f51550dbaa3d4c949fc4aea4 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Mon, 22 Dec 2025 18:43:46 +0100 Subject: [PATCH 3/5] Reorganize a bit: panels/ and subviews/ folders --- libs/@hashintel/petrinaut/src/constants/ui.ts | 24 +++++++++++++ .../src/views/Editor/editor-view.tsx | 8 ++--- .../BottomPanel/panel.tsx} | 25 ++++---------- .../LeftSideBar/floating-title.tsx | 0 .../LeftSideBar/hamburger-menu.tsx | 0 .../LeftSideBar/panel.tsx} | 20 +++-------- .../PropertiesPanel/color-select.tsx | 0 .../differential-equation-properties.tsx | 0 .../PropertiesPanel/initial-state-editor.tsx | 0 .../PropertiesPanel/multiple-selection.tsx | 0 .../PropertiesPanel/panel.tsx} | 0 .../PropertiesPanel/parameter-properties.tsx | 0 .../PropertiesPanel/place-properties.tsx | 0 .../PropertiesPanel/sortable-arc-item.tsx | 0 .../PropertiesPanel/transition-properties.tsx | 0 .../PropertiesPanel/type-properties.tsx | 0 .../visualizer-error-boundary.tsx | 0 .../diagnostics.tsx} | 30 ++++++++-------- .../differential-equations-list.tsx} | 28 +++++++-------- .../nodes-list.tsx} | 18 +++++----- .../parameters-list.tsx} | 20 +++++------ .../simulation-settings.tsx} | 16 ++++----- .../types-list.tsx} | 34 +++++++++---------- 23 files changed, 112 insertions(+), 111 deletions(-) rename libs/@hashintel/petrinaut/src/views/Editor/{components/BottomPanel/bottom-panel.tsx => panels/BottomPanel/panel.tsx} (83%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/LeftSideBar/floating-title.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/LeftSideBar/hamburger-menu.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/LeftSideBar/left-sidebar.tsx => panels/LeftSideBar/panel.tsx} (89%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/color-select.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/differential-equation-properties.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/initial-state-editor.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/multiple-selection.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/PropertiesPanel/properties-panel.tsx => panels/PropertiesPanel/panel.tsx} (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/parameter-properties.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/place-properties.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/sortable-arc-item.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/transition-properties.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/type-properties.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components => panels}/PropertiesPanel/visualizer-error-boundary.tsx (100%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/BottomPanel/diagnostics-content.tsx => subviews/diagnostics.tsx} (92%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/LeftSideBar/differential-equations-section.tsx => subviews/differential-equations-list.tsx} (88%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/LeftSideBar/nodes-section.tsx => subviews/nodes-list.tsx} (90%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/BottomPanel/parameters-content.tsx => subviews/parameters-list.tsx} (93%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/BottomPanel/simulation-settings-content.tsx => subviews/simulation-settings.tsx} (94%) rename libs/@hashintel/petrinaut/src/views/Editor/{components/LeftSideBar/types-section.tsx => subviews/types-list.tsx} (89%) diff --git a/libs/@hashintel/petrinaut/src/constants/ui.ts b/libs/@hashintel/petrinaut/src/constants/ui.ts index ddcc7960c50..2816ef61e75 100644 --- a/libs/@hashintel/petrinaut/src/constants/ui.ts +++ b/libs/@hashintel/petrinaut/src/constants/ui.ts @@ -2,6 +2,14 @@ * UI-related constants for the Petrinaut editor. */ +import type { SubView } from "../components/sub-view"; +import { diagnosticsSubView } from "../views/Editor/subviews/diagnostics"; +import { differentialEquationsListSubView } from "../views/Editor/subviews/differential-equations-list"; +import { nodesListSubView } from "../views/Editor/subviews/nodes-list"; +import { parametersListSubView } from "../views/Editor/subviews/parameters-list"; +import { simulationSettingsSubView } from "../views/Editor/subviews/simulation-settings"; +import { typesListSubView } from "../views/Editor/subviews/types-list"; + // Panel margin (spacing around panels) export const PANEL_MARGIN = 10; @@ -23,3 +31,19 @@ export const MAX_PROPERTIES_PANEL_WIDTH = 800; export const DEFAULT_BOTTOM_PANEL_HEIGHT = 180; export const MIN_BOTTOM_PANEL_HEIGHT = 100; export const MAX_BOTTOM_PANEL_HEIGHT = 600; + +// +// SubViews +// + +export const LEFT_SIDEBAR_SUBVIEWS: SubView[] = [ + typesListSubView, + differentialEquationsListSubView, + parametersListSubView, + nodesListSubView, +]; + +export const BOTTOM_PANEL_SUBVIEWS: SubView[] = [ + diagnosticsSubView, + simulationSettingsSubView, +]; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx index 4b4ba77e23c..e13704ee1cc 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx @@ -12,12 +12,12 @@ 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 { 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"; import { exportSDCPN } from "./lib/export-sdcpn"; import { importSDCPN } from "./lib/import-sdcpn"; +import { BottomPanel } from "./panels/BottomPanel/panel"; +import { LeftSideBar } from "./panels/LeftSideBar/panel"; +import { PropertiesPanel } from "./panels/PropertiesPanel/panel"; const fullHeightStyle = css({ height: "[100%]", @@ -71,7 +71,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/Editor/components/BottomPanel/bottom-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/panel.tsx similarity index 83% rename from libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/panel.tsx index f0797a2a7ea..fc6fae2f24c 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/bottom-panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/panel.tsx @@ -8,25 +8,14 @@ import { HorizontalTabsHeader, HorizontalTabsHeaderAction, } from "../../../../components/sub-view"; -import type { SubView } from "../../../../components/sub-view/types"; import { + BOTTOM_PANEL_SUBVIEWS, MAX_BOTTOM_PANEL_HEIGHT, MIN_BOTTOM_PANEL_HEIGHT, PANEL_MARGIN, } from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; import type { BottomPanelTab } from "../../../../state/editor-store"; -import { diagnosticsSubView } from "./diagnostics-content"; -import { parametersSubView } from "./parameters-content"; -import { simulationSettingsSubView } from "./simulation-settings-content"; - -// Pre-defined array of subviews for the bottom panel -// Note: Using explicit array typing due to TypeScript inference quirks with barrel imports -const BOTTOM_PANEL_SUBVIEWS: SubView[] = [ - diagnosticsSubView, - parametersSubView, - simulationSettingsSubView, -]; const glassPanelBaseStyle = css({ position: "fixed", @@ -89,9 +78,6 @@ export const BottomPanel: React.FC = () => { const setActiveTab = useEditorStore((state) => state.setActiveBottomPanelTab); const toggleBottomPanel = useEditorStore((state) => state.toggleBottomPanel); - // Use the pre-defined array of subviews - const subViews = BOTTOM_PANEL_SUBVIEWS; - // Handler for tab change that casts string to BottomPanelTab const handleTabChange = useCallback( (tabId: string) => { @@ -131,13 +117,13 @@ export const BottomPanel: React.FC = () => { {/* Tab Header */}
{/* Scrollable content */} - + ); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/floating-title.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/floating-title.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/floating-title.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/floating-title.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/hamburger-menu.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/hamburger-menu.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/hamburger-menu.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/hamburger-menu.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx similarity index 89% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx index 1bd5eed5efa..1fd3eb85cf7 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/left-sidebar.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx @@ -1,5 +1,4 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import { useMemo } from "react"; import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarRightCollapse, @@ -8,18 +7,15 @@ import { import { GlassPanel } from "../../../../components/glass-panel"; import type { MenuItem } from "../../../../components/menu"; import { VerticalSubViewsContainer } from "../../../../components/sub-view"; -import type { SubView } from "../../../../components/sub-view/types"; import { + LEFT_SIDEBAR_SUBVIEWS, MAX_LEFT_SIDEBAR_WIDTH, MIN_LEFT_SIDEBAR_WIDTH, PANEL_MARGIN, } from "../../../../constants/ui"; import { useEditorStore } from "../../../../state/editor-provider"; -import { differentialEquationsSectionSubView } from "./differential-equations-section"; import { FloatingTitle } from "./floating-title"; import { HamburgerMenu } from "./hamburger-menu"; -import { nodesSectionSubView } from "./nodes-section"; -import { typesSectionSubView } from "./types-section"; const outerContainerStyle = cva({ base: { @@ -158,16 +154,6 @@ export const LeftSideBar: React.FC = ({ (state) => state.setLeftSidebarWidth ); - // Define the subviews for the left sidebar - const subViews: SubView[] = useMemo( - () => [ - typesSectionSubView, - differentialEquationsSectionSubView, - nodesSectionSubView, - ], - [] - ); - return (
= ({
{/* Content sections - only visible when open */} - {isOpen && } + {isOpen && ( + + )}
); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/color-select.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/color-select.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/color-select.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/color-select.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/differential-equation-properties.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/differential-equation-properties.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/initial-state-editor.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/initial-state-editor.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/multiple-selection.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/multiple-selection.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/multiple-selection.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/multiple-selection.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/panel.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/properties-panel.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/panel.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/parameter-properties.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/place-properties.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/sortable-arc-item.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/sortable-arc-item.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/transition-properties.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/type-properties.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/visualizer-error-boundary.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/visualizer-error-boundary.tsx similarity index 100% rename from libs/@hashintel/petrinaut/src/views/Editor/components/PropertiesPanel/visualizer-error-boundary.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/visualizer-error-boundary.tsx diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/diagnostics.tsx similarity index 92% rename from libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/diagnostics.tsx index 75c3870f5e6..66dd35ff612 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/diagnostics-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/diagnostics.tsx @@ -3,10 +3,10 @@ import { useCallback, useMemo, useState } from "react"; import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; import ts from "typescript"; -import type { SubView } from "../../../../components/sub-view/types"; -import { useCheckerContext } from "../../../../state/checker-provider"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { useCheckerContext } from "../../../state/checker-provider"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; const emptyMessageStyle = css({ color: "core.gray.50", @@ -99,14 +99,14 @@ const positionStyle = css({ /** * Formats a TypeScript diagnostic message to a readable string */ -const formatDiagnosticMessage = ( - messageText: string | ts.DiagnosticMessageChain, -): string => { +function formatDiagnosticMessage( + messageText: string | ts.DiagnosticMessageChain +): string { if (typeof messageText === "string") { return messageText; } return ts.flattenDiagnosticMessageText(messageText, "\n"); -}; +} // --- Types --- @@ -126,15 +126,15 @@ interface GroupedDiagnostics { /** * DiagnosticsContent shows the full list of diagnostics grouped by entity. */ -export const DiagnosticsContent: React.FC = () => { +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 @@ -142,7 +142,7 @@ export const DiagnosticsContent: React.FC = () => { (entityId: string) => { setSelectedResourceId(entityId); }, - [setSelectedResourceId], + [setSelectedResourceId] ); // Group diagnostics by entity (transition or differential equation) @@ -158,14 +158,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"; @@ -293,7 +293,7 @@ export const DiagnosticsContent: React.FC = () => { }; /** - * SubView definition for Diagnostics tab. + * SubView definition for Diagnostics. */ export const diagnosticsSubView: SubView = { id: "diagnostics", diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx similarity index 88% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx index d29ce47b70d..9b7eb8bf45a 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/differential-equations-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx @@ -1,11 +1,11 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { v4 as uuidv4 } from "uuid"; -import type { SubView } from "../../../../components/sub-view/types"; -import { DEFAULT_DIFFERENTIAL_EQUATION_CODE } from "../../../../core/default-codes"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { DEFAULT_DIFFERENTIAL_EQUATION_CODE } from "../../../core/default-codes"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; +import { useSimulationStore } from "../../../state/simulation-provider"; const listContainerStyle = css({ display: "flex", @@ -112,17 +112,17 @@ const addButtonStyle = css({ * DifferentialEquationsSectionContent displays the list of differential equations. * This is the content portion without the collapsible header. */ -export const DifferentialEquationsSectionContent: React.FC = () => { +const DifferentialEquationsSectionContent: React.FC = () => { const { petriNetDefinition: { differentialEquations }, removeDifferentialEquation, } = useSDCPNContext(); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, + (state) => state.selectedResourceId ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); // Check if simulation is running or paused @@ -167,7 +167,7 @@ export const DifferentialEquationsSectionContent: React.FC = () => { if ( // eslint-disable-next-line no-alert window.confirm( - `Delete equation "${eq.name}"? Any places referencing this equation will have their differential equation reset.`, + `Delete equation "${eq.name}"? Any places referencing this equation will have their differential equation reset.` ) ) { removeDifferentialEquation(eq.id); @@ -191,13 +191,13 @@ export const DifferentialEquationsSectionContent: React.FC = () => { /** * DifferentialEquationsSectionHeaderAction renders the add button for the section header. */ -export const DifferentialEquationsSectionHeaderAction: React.FC = () => { +const DifferentialEquationsSectionHeaderAction: React.FC = () => { const { petriNetDefinition: { types, differentialEquations }, addDifferentialEquation, } = useSDCPNContext(); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); // Check if simulation is running or paused @@ -229,10 +229,10 @@ export const DifferentialEquationsSectionHeaderAction: React.FC = () => { }; /** - * SubView definition for Differential Equations section. + * SubView definition for Differential Equations list. */ -export const differentialEquationsSectionSubView: SubView = { - id: "differential-equations", +export const differentialEquationsListSubView: SubView = { + id: "differential-equations-list", title: "Differential Equations", tooltip: `Differential equations govern how token data changes over time when tokens remain in a place ("dynamics").`, component: DifferentialEquationsSectionContent, diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx similarity index 90% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx index e2bc691b497..f5db291b4bd 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/nodes-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx @@ -1,9 +1,9 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { FaCircle, FaSquare } from "react-icons/fa6"; -import type { SubView } from "../../../../components/sub-view/types"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; const listContainerStyle = css({ display: "flex", @@ -87,15 +87,15 @@ const emptyMessageStyle = css({ * NodesSectionContent displays the list of places and transitions. * This is the content portion without the collapsible header. */ -export const NodesSectionContent: React.FC = () => { +const NodesSectionContent: React.FC = () => { const { petriNetDefinition: { places, transitions }, } = useSDCPNContext(); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, + (state) => state.selectedResourceId ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); const handleLayerClick = (id: string) => { @@ -164,10 +164,10 @@ export const NodesSectionContent: React.FC = () => { }; /** - * SubView definition for Nodes section. + * SubView definition for Nodes list. */ -export const nodesSectionSubView: SubView = { - id: "nodes", +export const nodesListSubView: SubView = { + id: "nodes-list", title: "Nodes", tooltip: "Manage nodes in the net, including places and transitions. Places represent states in the net, and transitions represent events which change the state of the net.", diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx similarity index 93% rename from libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx index 9e04e425d32..7c2965db0f1 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/parameters-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx @@ -1,10 +1,10 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { v4 as uuidv4 } from "uuid"; -import type { SubView } from "../../../../components/sub-view/types"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; +import { useSimulationStore } from "../../../state/simulation-provider"; const addButtonStyle = css({ display: "flex", @@ -180,9 +180,9 @@ const ParametersHeaderAction: React.FC = () => { }; /** - * ParametersContent displays global parameters in the BottomPanel. + * ParametersList displays global parameters list as a SubView. */ -export const ParametersContent: React.FC = () => { +const ParametersList: React.FC = () => { const { petriNetDefinition: { parameters }, removeParameter, @@ -288,13 +288,13 @@ export const ParametersContent: React.FC = () => { }; /** - * SubView definition for Global Parameters tab. + * SubView definition for Global Parameters List. */ -export const parametersSubView: SubView = { - id: "parameters", +export const parametersListSubView: SubView = { + id: "parameters-list", title: "Global Parameters", tooltip: "Parameters are injected into dynamics, lambda, and kernel functions.", - component: ParametersContent, + component: ParametersList, renderHeaderAction: () => , }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/simulation-settings.tsx similarity index 94% rename from libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/simulation-settings.tsx index bfe309fd396..91bc1f97bcd 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomPanel/simulation-settings-content.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/simulation-settings.tsx @@ -2,11 +2,11 @@ import { css } from "@hashintel/ds-helpers/css"; import { useState } from "react"; import { TbArrowRight } from "react-icons/tb"; -import type { SubView } from "../../../../components/sub-view/types"; -import { InfoIconTooltip } from "../../../../components/tooltip"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { InfoIconTooltip } from "../../../components/tooltip"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; +import { useSimulationStore } from "../../../state/simulation-provider"; const containerStyle = css({ display: "flex", @@ -190,7 +190,7 @@ const editButtonIconStyle = css({ * SimulationSettingsContent displays simulation settings in the BottomPanel. * Split into two sections: Computation and Parameters. */ -export const SimulationSettingsContent: React.FC = () => { +const SimulationSettingsContent: React.FC = () => { const setGlobalMode = useEditorStore((state) => state.setGlobalMode); const simulationState = useSimulationStore((state) => state.state); const simulationError = useSimulationStore((state) => state.error); @@ -198,11 +198,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/types-section.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx similarity index 89% rename from libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx rename to libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx index 4c53a717abb..c0059562b01 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/LeftSideBar/types-section.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx @@ -1,9 +1,9 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import type { SubView } from "../../../../components/sub-view/types"; -import { useEditorStore } from "../../../../state/editor-provider"; -import { useSDCPNContext } from "../../../../state/sdcpn-provider"; -import { useSimulationStore } from "../../../../state/simulation-provider"; +import type { SubView } from "../../../components/sub-view/types"; +import { useEditorStore } from "../../../state/editor-provider"; +import { useSDCPNContext } from "../../../state/sdcpn-provider"; +import { useSimulationStore } from "../../../state/simulation-provider"; const listContainerStyle = css({ display: "flex", @@ -133,18 +133,18 @@ const TYPE_COLOR_POOL = [ * Get the next available color from the pool that's not currently in use. * If all colors are in use, cycle back to the beginning. */ -const getNextAvailableColor = (existingColors: string[]): string => { +function getNextAvailableColor(existingColors: string[]): string { const unusedColor = TYPE_COLOR_POOL.find( - (color) => !existingColors.includes(color), + (color) => !existingColors.includes(color) ); return unusedColor ?? TYPE_COLOR_POOL[0]!; -}; +} /** * Extract the highest type number from existing type names. * Looks for patterns like "Type 1", "Type 2", "New Type 3", etc. */ -const getNextTypeNumber = (existingNames: string[]): number => { +function getNextTypeNumber(existingNames: string[]): number { let maxNumber = 0; for (const name of existingNames) { // Match patterns like "Type 1", "New Type 2", etc. @@ -157,23 +157,23 @@ const getNextTypeNumber = (existingNames: string[]): number => { } } return maxNumber + 1; -}; +} /** * TypesSectionContent displays the list of token types. * This is the content portion without the collapsible header. */ -export const TypesSectionContent: React.FC = () => { +const TypesSectionContent: React.FC = () => { const { petriNetDefinition: { types }, removeType, } = useSDCPNContext(); const selectedResourceId = useEditorStore( - (state) => state.selectedResourceId, + (state) => state.selectedResourceId ); const setSelectedResourceId = useEditorStore( - (state) => state.setSelectedResourceId, + (state) => state.setSelectedResourceId ); // Check if simulation is running or paused @@ -220,7 +220,7 @@ export const TypesSectionContent: React.FC = () => { if ( // eslint-disable-next-line no-alert window.confirm( - `Delete token type "${type.name}"? All places using this type will have their type set to null.`, + `Delete token type "${type.name}"? All places using this type will have their type set to null.` ) ) { removeType(type.id); @@ -244,7 +244,7 @@ export const TypesSectionContent: React.FC = () => { /** * TypesSectionHeaderAction renders the add button for the types section header. */ -export const TypesSectionHeaderAction: React.FC = () => { +const TypesSectionHeaderAction: React.FC = () => { const { petriNetDefinition: { types }, addType, @@ -289,10 +289,10 @@ export const TypesSectionHeaderAction: React.FC = () => { }; /** - * SubView definition for Token Types section. + * SubView definition for Token Types list. */ -export const typesSectionSubView: SubView = { - id: "token-types", +export const typesListSubView: SubView = { + id: "token-types-list", title: "Token Types", tooltip: "Manage data types which can be assigned to tokens in a place.", component: TypesSectionContent, From 9a4403fbc1e47c975b09be09084dead02e9cfa8a Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 01:40:58 +0100 Subject: [PATCH 4/5] PARTIAL: WIP resizable SubView and Place state and visualizer output Subviews --- .../src/components/sub-view/index.ts | 2 +- .../src/components/sub-view/types.ts | 18 +- .../sub-view/vertical-sub-views-container.tsx | 228 ++++- .../PropertiesPanel/initial-state-editor.tsx | 42 +- .../place-properties-context.tsx | 61 ++ .../PropertiesPanel/place-properties.tsx | 822 +++++++----------- .../subviews/differential-equations-list.tsx | 6 +- .../src/views/Editor/subviews/nodes-list.tsx | 6 +- .../views/Editor/subviews/parameters-list.tsx | 5 + .../Editor/subviews/place-initial-state.tsx | 115 +++ .../subviews/place-visualizer-output.tsx | 158 ++++ .../src/views/Editor/subviews/types-list.tsx | 6 +- 12 files changed, 916 insertions(+), 553 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties-context.tsx create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx create mode 100644 libs/@hashintel/petrinaut/src/views/Editor/subviews/place-visualizer-output.tsx diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/index.ts b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts index 804681cb1c9..d665394075c 100644 --- a/libs/@hashintel/petrinaut/src/components/sub-view/index.ts +++ b/libs/@hashintel/petrinaut/src/components/sub-view/index.ts @@ -1,4 +1,4 @@ -export type { SubView } from "./types"; +export type { SubView, SubViewResizeConfig } from "./types"; export { VerticalSubViewsContainer } from "./vertical-sub-views-container"; export { HorizontalTabsContainer, diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/types.ts b/libs/@hashintel/petrinaut/src/components/sub-view/types.ts index 3367800a192..afe0448c7db 100644 --- a/libs/@hashintel/petrinaut/src/components/sub-view/types.ts +++ b/libs/@hashintel/petrinaut/src/components/sub-view/types.ts @@ -1,5 +1,17 @@ import type { ComponentType, ReactNode } from "react"; +/** + * Configuration for resizable subviews. + */ +export interface SubViewResizeConfig { + /** Default height when expanded (in pixels) */ + defaultHeight: number; + /** Minimum height constraint (in pixels) */ + minHeight?: number; + /** Maximum height constraint (in pixels) */ + maxHeight?: number; +} + /** * SubView represents a single view that can be displayed in either: * - A vertical collapsible section (LeftSideBar) @@ -26,5 +38,9 @@ export interface SubView { * Only affects vertical layout. Defaults to false. */ flexGrow?: boolean; + /** + * Configuration for making the subview resizable when expanded. + * Only affects vertical layout. When set, the section can be resized by dragging its bottom edge. + */ + resizable?: SubViewResizeConfig; } - diff --git a/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx b/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx index b2a580142da..69754d3c90e 100644 --- a/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx +++ b/libs/@hashintel/petrinaut/src/components/sub-view/vertical-sub-views-container.tsx @@ -1,25 +1,30 @@ import { css, cva } from "@hashintel/ds-helpers/css"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { FaChevronDown, FaChevronRight } from "react-icons/fa6"; import { InfoIconTooltip } from "../tooltip"; -import type { SubView } from "./types"; +import type { SubView, SubViewResizeConfig } from "./types"; const sectionContainerStyle = cva({ base: { display: "flex", flexDirection: "column", - gap: "[8px]", + gap: "[4px]", }, variants: { + hasBottomPadding: { + true: { + paddingBottom: "[8px]", + }, + false: { + paddingBottom: "[0]", + }, + }, flexGrow: { true: { flex: "[1]", minHeight: "[0]", }, - false: { - paddingBottom: "[16px]", - }, }, }, }); @@ -66,10 +71,145 @@ const contentContainerStyle = cva({ }, }); +const resizableContentStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[2px]", + overflowY: "auto", +}); + +const resizeHandleStyle = cva({ + base: { + height: "[6px]", + cursor: "ns-resize", + backgroundColor: "[transparent]", + border: "none", + padding: "[0]", + borderRadius: "[3px]", + transition: "[background-color 0.15s ease]", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.08)]", + }, + }, + variants: { + isResizing: { + true: { + backgroundColor: "[rgba(59, 130, 246, 0.3)]", + }, + }, + position: { + top: { + marginBottom: "[2px]", + }, + bottom: { + marginTop: "[2px]", + }, + }, + }, +}); + +/** + * Custom hook for resize logic that can be used at any component level. + */ +const useResizable = ( + config: SubViewResizeConfig, + handlePosition: "top" | "bottom" +) => { + const [height, setHeight] = useState(config.defaultHeight); + const [isResizing, setIsResizing] = useState(false); + const resizeStartY = useRef(0); + const resizeStartHeight = useRef(0); + + const handleResizeStart = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + setIsResizing(true); + resizeStartY.current = event.clientY; + resizeStartHeight.current = height; + }, + [height] + ); + + const handleResizeMove = useCallback( + (event: MouseEvent) => { + if (!isResizing) { + return; + } + + const delta = event.clientY - resizeStartY.current; + // When handle is at top, invert the delta: dragging up should increase height + const effectiveDelta = handlePosition === "top" ? -delta : delta; + const newHeight = resizeStartHeight.current + effectiveDelta; + + const minHeight = config.minHeight ?? 100; + const maxHeight = config.maxHeight ?? 600; + + setHeight(Math.max(minHeight, Math.min(maxHeight, newHeight))); + }, + [isResizing, config.minHeight, config.maxHeight, handlePosition] + ); + + const handleResizeEnd = useCallback(() => { + setIsResizing(false); + }, []); + + // Global event listeners during resize + useEffect(() => { + if (!isResizing) { + return; + } + + document.addEventListener("mousemove", handleResizeMove); + document.addEventListener("mouseup", handleResizeEnd); + document.body.style.cursor = "ns-resize"; + document.body.style.userSelect = "none"; + + return () => { + document.removeEventListener("mousemove", handleResizeMove); + document.removeEventListener("mouseup", handleResizeEnd); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + }, [isResizing, handleResizeMove, handleResizeEnd]); + + return { height, isResizing, handleResizeStart }; +}; + +interface ResizeHandleProps { + position: "top" | "bottom"; + isResizing: boolean; + onMouseDown: (event: React.MouseEvent) => void; +} + +/** + * Reusable resize handle button component. + */ +const ResizeHandle: React.FC = ({ + position, + isResizing, + onMouseDown, +}) => ( +
- {isExpanded && ( -
- -
- )} + {isExpanded && renderContent()}
); }; @@ -137,6 +326,12 @@ interface VerticalSubViewsContainerProps { subViews: SubView[]; /** Whether sections should be expanded by default */ defaultExpanded?: boolean; + /** + * Position of resize handles for resizable sections. + * Use 'top' when the container is aligned to the bottom of its parent. + * Defaults to 'bottom'. + */ + resizeHandlePosition?: "top" | "bottom"; } /** @@ -145,7 +340,7 @@ interface VerticalSubViewsContainerProps { */ export const VerticalSubViewsContainer: React.FC< VerticalSubViewsContainerProps -> = ({ subViews, defaultExpanded = true }) => { +> = ({ subViews, defaultExpanded = true, resizeHandlePosition = "bottom" }) => { return ( <> {subViews.map((subView, index) => ( @@ -154,6 +349,7 @@ export const VerticalSubViewsContainer: React.FC< subView={subView} defaultExpanded={defaultExpanded} isLast={index === subViews.length - 1} + resizeHandlePosition={resizeHandlePosition} /> ))} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx index e41aaaf899c..0b6e43d57dc 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/initial-state-editor.tsx @@ -2,7 +2,6 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { useEffect, useRef, useState } from "react"; import { TbTrash } from "react-icons/tb"; -import { InfoIconTooltip } from "../../../../components/tooltip"; import type { Color } from "../../../../core/types/sdcpn"; import { useSimulationStore } from "../../../../state/simulation-provider"; @@ -14,11 +13,6 @@ const headerRowStyle = css({ height: "[20px]", }); -const headerLabelStyle = css({ - fontWeight: 500, - fontSize: "[12px]", -}); - const clearButtonStyle = css({ fontSize: "[11px]", padding: "[2px 8px]", @@ -250,16 +244,16 @@ export const InitialStateEditor: React.FC = ({ const { height, isResizing, containerRef, startResize } = useResizable(250); const isSimulationNotRun = useSimulationStore( - (state) => state.state === "NotRun", + (state) => state.state === "NotRun" ); const initialMarking = useSimulationStore((state) => state.initialMarking); const setInitialMarking = useSimulationStore( - (state) => state.setInitialMarking, + (state) => state.setInitialMarking ); const simulation = useSimulationStore((state) => state.simulation); const currentlyViewedFrame = useSimulationStore( - (state) => state.currentlyViewedFrame, + (state) => state.currentlyViewedFrame ); // Determine if we should show current simulation state or initial marking @@ -305,7 +299,7 @@ export const InitialStateEditor: React.FC = ({ const tokenValues: number[] = []; for (let colIndex = 0; colIndex < dimensions; colIndex++) { tokenValues.push( - currentMarking.values[i * dimensions + colIndex] ?? 0, + currentMarking.values[i * dimensions + colIndex] ?? 0 ); } tokens.push(tokenValues); @@ -338,7 +332,7 @@ export const InitialStateEditor: React.FC = ({ const tokenValues: number[] = []; for (let colIndex = 0; colIndex < dimensions; colIndex++) { tokenValues.push( - currentMarking.values[i * dimensions + colIndex] ?? 0, + currentMarking.values[i * dimensions + colIndex] ?? 0 ); } tokens.push(tokenValues); @@ -381,7 +375,7 @@ export const InitialStateEditor: React.FC = ({ } } else { newData = prev.map((rowData, index) => - index === row ? [...rowData] : rowData, + index === row ? [...rowData] : rowData ); if (newData[row]) { newData[row][col] = value; @@ -406,7 +400,7 @@ export const InitialStateEditor: React.FC = ({ // Focus the row number cell after state update setTimeout(() => { const rowCell = document.querySelector( - `td[data-row="${newData.length - 1}"]`, + `td[data-row="${newData.length - 1}"]` ); if (rowCell instanceof HTMLElement) { rowCell.focus(); @@ -418,7 +412,7 @@ export const InitialStateEditor: React.FC = ({ // Focus the row number cell after state update setTimeout(() => { const rowCell = document.querySelector( - `td[data-row="${rowIndex}"]`, + `td[data-row="${rowIndex}"]` ); if (rowCell instanceof HTMLElement) { rowCell.focus(); @@ -444,7 +438,7 @@ export const InitialStateEditor: React.FC = ({ const handleKeyDown = ( event: React.KeyboardEvent, row: number, - col: number, + col: number ) => { if (hasSimulation) { return; @@ -514,7 +508,7 @@ export const InitialStateEditor: React.FC = ({ }); setTimeout(() => { const prevCell = cellRefs.current.get( - `${row - 1}-${placeType.elements.length - 1}`, + `${row - 1}-${placeType.elements.length - 1}` ); prevCell?.focus(); }, 0); @@ -607,7 +601,7 @@ export const InitialStateEditor: React.FC = ({ setFocusedCell({ row: row - 1, col: placeType.elements.length - 1 }); setTimeout(() => { const prevCell = cellRefs.current.get( - `${row - 1}-${placeType.elements.length - 1}`, + `${row - 1}-${placeType.elements.length - 1}` ); prevCell?.focus(); }, 0); @@ -718,7 +712,7 @@ export const InitialStateEditor: React.FC = ({ setEditingCell(null); // Focus the next row number cell const nextRowCell = document.querySelector( - `td[data-row="${rowIndex + 1}"]`, + `td[data-row="${rowIndex + 1}"]` ); if (nextRowCell instanceof HTMLElement) { nextRowCell.focus(); @@ -731,7 +725,7 @@ export const InitialStateEditor: React.FC = ({ setEditingCell(null); // Focus the previous row number cell const prevRowCell = document.querySelector( - `td[data-row="${rowIndex - 1}"]`, + `td[data-row="${rowIndex - 1}"]` ); if (prevRowCell instanceof HTMLElement) { prevRowCell.focus(); @@ -744,12 +738,6 @@ export const InitialStateEditor: React.FC = ({ return (
-
- {isSimulationNotRun ? "Initial State" : "State"} - {isSimulationNotRun && ( - - )} -
{isSimulationNotRun && tableData.length > 0 && ( - + {/* Scrollable content section */} +
+
+
+
Place
+ + + +
-
- -
-
Name
- { - setNameInputValue(event.target.value); - // Clear error when user starts typing - if (nameError) { - setNameError(null); - } - }} - onFocus={() => setIsNameInputFocused(true)} - onBlur={() => { - setIsNameInputFocused(false); - handleNameBlur(); - }} - disabled={isReadOnly} - className={inputStyle({ isReadOnly, hasError: !!nameError })} - /> - {nameError &&
{nameError}
} -
-
-
- Accepted token type - +
Name
+ { + setNameInputValue(event.target.value); + // Clear error when user starts typing + if (nameError) { + setNameError(null); + } + }} + onFocus={() => setIsNameInputFocused(true)} + onBlur={() => { + setIsNameInputFocused(false); + handleNameBlur(); + }} + disabled={isReadOnly} + className={inputStyle({ isReadOnly, hasError: !!nameError })} /> + {nameError &&
{nameError}
}
- - - {place.colorId && ( -
- -
- )} -
-
-
-
- { - updatePlace(place.id, (existingPlace) => { - existingPlace.dynamicsEnabled = checked; - }); - }} +
+
+ Accepted token type +
-
- Dynamics - -
-
- {(place.colorId === null || availableDiffEqs.length === 0) && ( -
- {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"} -
- )} -
- - {place.colorId && - place.dynamicsEnabled && - availableDiffEqs.length > 0 && ( -
-
Differential Equation
- - - {place.differentialEquationId && ( -
- -
- )} -
- )} - - {/* Initial State section - shown in both Edit and Simulate modes */} - {(() => { - const placeType = place.colorId - ? types.find((tp) => tp.id === place.colorId) - : null; - - // Determine if simulation is running (has frames) - const hasSimulationFrames = - simulation !== null && simulation.frames.length > 0; - - // If no type or type has 0 dimensions, show simple number input - if (!placeType || placeType.elements.length === 0) { - // Get token count from simulation frame or initial marking - let currentTokenCount = 0; - if (hasSimulationFrames) { - const currentFrame = simulation.frames[currentlyViewedFrame]; - if (currentFrame) { - const placeState = currentFrame.places.get(place.id); - currentTokenCount = placeState?.count ?? 0; - } - } else { - const currentMarking = initialMarking.get(place.id); - currentTokenCount = currentMarking?.count ?? 0; - } - - return ( -
-
- {hasSimulationFrames ? "State" : "Initial 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={hasSimulationFrames} - className={inputStyle({ isReadOnly: hasSimulationFrames })} - /> -
+ + + {place.colorId && ( +
+
- ); - } - - return ( - - ); - })()} + )} +
- {/* Visualizer section */} - {globalMode === "edit" && (
{ - if (checked) { - // Turning on: use saved code if available, otherwise default - updatePlace(place.id, (existingPlace) => { - existingPlace.visualizerCode = - savedVisualizerCode ?? DEFAULT_VISUALIZER_CODE; - }); - } else { - // Turning off: save current code and set to undefined - if (place.visualizerCode) { - setSavedVisualizerCode(place.visualizerCode); - } - updatePlace(place.id, (existingPlace) => { - existingPlace.visualizerCode = undefined; - }); - } + updatePlace(place.id, (existingPlace) => { + existingPlace.dynamicsEnabled = checked; + }); }} />
- Visualizer - + Dynamics +
+ {(place.colorId === null || availableDiffEqs.length === 0) && ( +
+ {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"} +
+ )}
- )} - {place.visualizerCode !== undefined && ( -
- {(() => { - // Determine if we should show visualization (when simulation has frames) - const hasSimulationFrames = - simulation !== null && simulation.frames.length > 0; - const showVisualization = isReadOnly || hasSimulationFrames; - - return ( - <> -
-
- {showVisualization - ? "Visualizer Output" - : "Visualizer Code"} -
- {!showVisualization && ( - - - - } - 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 - }, - }, - ]} - /> - )} + {place.colorId && + place.dynamicsEnabled && + availableDiffEqs.length > 0 && ( +
+
Differential Equation
+ + + {place.differentialEquationId && ( +
+
-
- {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; - - 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); - } - - // 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, - ); + )} +
+ )} + + {/* Visualizer toggle - only shown in edit mode */} + {globalMode === "edit" && ( +
+
+
+ { + if (checked) { + // Turning on: use saved code if available, otherwise default + updatePlace(place.id, (existingPlace) => { + existingPlace.visualizerCode = + savedVisualizerCode ?? DEFAULT_VISUALIZER_CODE; + }); + } else { + // Turning off: save current code and set to undefined + if (place.visualizerCode) { + setSavedVisualizerCode(place.visualizerCode); } + updatePlace(place.id, (existingPlace) => { + existingPlace.visualizerCode = undefined; + }); + } + }} + /> +
+
+ Visualizer + +
+
+
+ )} - // Render the compiled visualizer component - if (!VisualizerComponent) { - return ( -
- Failed to compile visualizer code. Check console for - errors. -
- ); - } + {/* Visualizer Code Editor - only shown in edit mode when visualizer is enabled */} + {place.visualizerCode !== undefined && !showVisualizerOutput && ( +
+
+
Visualizer Code
+ + + + } + items={[ + { + id: "load-default", + label: "Load default template", + onClick: () => { + // Get the place's type to generate appropriate default code + const currentPlaceType = place.colorId + ? types.find((t) => t.id === place.colorId) + : null; - 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, - }} - /> - )} -
- - ); - })()} -
- )} + updatePlace(place.id, (existingPlace) => { + existingPlace.visualizerCode = currentPlaceType + ? generateDefaultVisualizerCode(currentPlaceType) + : DEFAULT_VISUALIZER_CODE; + }); + }, + }, + { + id: "generate-ai", + label: ( + +
+ + Generate with AI +
+
+ ), + disabled: true, + onClick: () => { + // TODO: Implement AI generation + }, + }, + ]} + /> +
+
+ { + 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, + }} + /> +
+
+ )} +
-
+ {/* Fixed SubViews at bottom: Initial State and Visualizer Output */} +
+ + + +
); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx index 9b7eb8bf45a..04757212b06 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx @@ -237,5 +237,9 @@ export const differentialEquationsListSubView: SubView = { tooltip: `Differential equations govern how token data changes over time when tokens remain in a place ("dynamics").`, component: DifferentialEquationsSectionContent, renderHeaderAction: () => , - flexGrow: false, + resizable: { + defaultHeight: 100, + minHeight: 60, + maxHeight: 250, + }, }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx index f5db291b4bd..283db158397 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/nodes-list.tsx @@ -172,5 +172,9 @@ export const nodesListSubView: SubView = { tooltip: "Manage nodes in the net, including places and transitions. Places represent states in the net, and transitions represent events which change the state of the net.", component: NodesSectionContent, - flexGrow: true, + resizable: { + defaultHeight: 150, + minHeight: 80, + maxHeight: 400, + }, }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx index 7c2965db0f1..bf5fc7cb3f9 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx @@ -297,4 +297,9 @@ export const parametersListSubView: SubView = { "Parameters are injected into dynamics, lambda, and kernel functions.", component: ParametersList, renderHeaderAction: () => , + resizable: { + defaultHeight: 100, + minHeight: 60, + maxHeight: 250, + }, }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx new file mode 100644 index 00000000000..e071c7f22a1 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx @@ -0,0 +1,115 @@ +import { css } from "@hashintel/ds-helpers/css"; + +import type { SubView } from "../../../components/sub-view/types"; +import { useSimulationStore } from "../../../state/simulation-provider"; +import { InitialStateEditor } from "../panels/PropertiesPanel/initial-state-editor"; +import { usePlacePropertiesContext } from "../panels/PropertiesPanel/place-properties-context"; + +const inputStyle = css({ + fontSize: "[14px]", + padding: "[6px 8px]", + borderRadius: "[4px]", + width: "[100%]", + boxSizing: "border-box", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + cursor: "not-allowed", +}); + +const fieldLabelStyle = css({ + fontWeight: 500, + fontSize: "[12px]", + marginBottom: "[4px]", +}); + +const simpleStateContainerStyle = css({ + display: "flex", + flexDirection: "column", + gap: "[8px]", +}); + +/** + * PlaceInitialStateContent - Renders the initial state editor for a place. + * Uses PlacePropertiesContext to access the current place data. + */ +const PlaceInitialStateContent: React.FC = () => { + const { place, placeType } = usePlacePropertiesContext(); + + const simulation = useSimulationStore((state) => state.simulation); + const initialMarking = useSimulationStore((state) => state.initialMarking); + const setInitialMarking = useSimulationStore( + (state) => state.setInitialMarking + ); + const currentlyViewedFrame = useSimulationStore( + (state) => state.currentlyViewedFrame + ); + + // Determine if simulation is running (has frames) + const hasSimulationFrames = + simulation !== null && simulation.frames.length > 0; + + // If no type or type has 0 dimensions, show simple number input + if (!placeType || placeType.elements.length === 0) { + // Get token count from simulation frame or initial marking + let currentTokenCount = 0; + if (hasSimulationFrames) { + const currentFrame = simulation.frames[currentlyViewedFrame]; + if (currentFrame) { + const placeState = currentFrame.places.get(place.id); + currentTokenCount = placeState?.count ?? 0; + } + } else { + const currentMarking = initialMarking.get(place.id); + currentTokenCount = currentMarking?.count ?? 0; + } + + return ( +
+
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={hasSimulationFrames} + className={inputStyle} + /> +
+ ); + } + + return ( + + ); +}; + +/** + * SubView definition for Place Initial State. + * Note: This subview requires PlacePropertiesProvider to be in the component tree. + */ +export const placeInitialStateSubView: SubView = { + id: "place-initial-state", + title: "State", + tooltip: + "Define the initial tokens in this place. During simulation, shows current state.", + component: PlaceInitialStateContent, + resizable: { + defaultHeight: 150, + minHeight: 80, + maxHeight: 400, + }, +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-visualizer-output.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-visualizer-output.tsx new file mode 100644 index 00000000000..ee78bec7764 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-visualizer-output.tsx @@ -0,0 +1,158 @@ +import { css } from "@hashintel/ds-helpers/css"; +import { useMemo } from "react"; + +import type { SubView } from "../../../components/sub-view/types"; +import { compileVisualizer } from "../../../core/simulation/compile-visualizer"; +import { + mergeParameterValues, + useDefaultParameterValues, +} from "../../../hooks/use-default-parameter-values"; +import { useSimulationStore } from "../../../state/simulation-provider"; +import { usePlacePropertiesContext } from "../panels/PropertiesPanel/place-properties-context"; +import { VisualizerErrorBoundary } from "../panels/PropertiesPanel/visualizer-error-boundary"; + +const visualizerMessageStyle = css({ + padding: "[12px]", + color: "[#666]", +}); + +const visualizerErrorStyle = css({ + padding: "[12px]", + color: "[#d32f2f]", +}); + +/** + * PlaceVisualizerOutputContent - Renders the visualizer output for a place. + * Uses PlacePropertiesContext to access the current place data. + */ +const PlaceVisualizerOutputContent: React.FC = () => { + const { place, placeType } = usePlacePropertiesContext(); + + const simulation = useSimulationStore((state) => state.simulation); + const initialMarking = useSimulationStore((state) => state.initialMarking); + const parameterValues = useSimulationStore((state) => state.parameterValues); + const currentlyViewedFrame = useSimulationStore( + (state) => state.currentlyViewedFrame + ); + + // Get default parameter values from SDCPN definition + const defaultParameterValues = useDefaultParameterValues(); + + // Compile visualizer code once when it changes + const VisualizerComponent = useMemo(() => { + if (!place.visualizerCode) { + return null; + } + + try { + return compileVisualizer(place.visualizerCode); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to compile visualizer code:", error); + return null; + } + }, [place.visualizerCode]); + + // If no visualizer code, show nothing + if (!place.visualizerCode) { + return ( +
No visualizer code defined
+ ); + } + + // Get place type to determine dimensions + 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 tokenIndex = 0; tokenIndex < count; tokenIndex++) { + const token: Record = {}; + for (let colIndex = 0; colIndex < dimensions; colIndex++) { + const dimensionName = placeType.elements[colIndex]!.name; + token[dimensionName] = + tokenValues[tokenIndex * 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 tokenIndex = 0; tokenIndex < marking.count; tokenIndex++) { + const token: Record = {}; + for (let colIndex = 0; colIndex < dimensions; colIndex++) { + const dimensionName = placeType.elements[colIndex]!.name; + token[dimensionName] = + marking.values[tokenIndex * 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. +
+ ); + } + + return ( + + + + ); +}; + +/** + * SubView definition for Place Visualizer Output. + * Note: This subview requires PlacePropertiesProvider to be in the component tree. + */ +export const placeVisualizerOutputSubView: SubView = { + id: "place-visualizer-output", + title: "Visualizer Output", + tooltip: + "Custom visualization of tokens in this place, defined by the visualizer code.", + component: PlaceVisualizerOutputContent, + resizable: { + defaultHeight: 200, + minHeight: 100, + maxHeight: 500, + }, +}; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx index c0059562b01..2b066944149 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx @@ -297,5 +297,9 @@ export const typesListSubView: SubView = { tooltip: "Manage data types which can be assigned to tokens in a place.", component: TypesSectionContent, renderHeaderAction: () => , - flexGrow: false, + resizable: { + defaultHeight: 120, + minHeight: 60, + maxHeight: 300, + }, }; From 1043468f104bb74efeab0ab4501901d427f73d91 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 23 Dec 2025 11:17:07 +0100 Subject: [PATCH 5/5] Update LeftSideBar gap between subviews --- .../petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx index 1fd3eb85cf7..ec5324e7a45 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/LeftSideBar/panel.tsx @@ -49,7 +49,7 @@ const panelContentStyle = cva({ height: "[100%]", padding: "[16px]", flexDirection: "column", - gap: "[16px]", + gap: "[4px]", alignItems: "stretch", }, false: {