diff --git a/.changeset/fix-readonly-tooltips.md b/.changeset/fix-readonly-tooltips.md new file mode 100644 index 00000000000..b84fd5e33d0 --- /dev/null +++ b/.changeset/fix-readonly-tooltips.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Fix read-only tooltips to always show during simulation mode diff --git a/libs/@hashintel/petrinaut/src/components/button.tsx b/libs/@hashintel/petrinaut/src/components/button.tsx new file mode 100644 index 00000000000..0fef0ad72d6 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/button.tsx @@ -0,0 +1,82 @@ +import { cva } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const buttonStyle = cva({ + base: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + gap: "[6px]", + fontSize: "[12px]", + padding: "[4px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.2)]", + borderRadius: "[4px]", + backgroundColor: "[white]", + color: "[#333]", + cursor: "pointer", + transition: "[all 0.15s ease]", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + }, + _active: { + backgroundColor: "[rgba(0, 0, 0, 0.1)]", + }, + }, + variants: { + isDisabled: { + true: { + opacity: "[0.5]", + cursor: "not-allowed", + _hover: { + backgroundColor: "[white]", + }, + }, + false: {}, + }, + variant: { + default: {}, + ghost: { + border: "none", + backgroundColor: "[transparent]", + _hover: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + }, + }, + }, + }, + defaultVariants: { + isDisabled: false, + variant: "default", + }, +}); + +type ButtonProps = React.ButtonHTMLAttributes & { + /** Button variant */ + variant?: "default" | "ghost"; + /** Button content */ + children: React.ReactNode; + /** Ref to the button element */ + ref?: React.Ref; +}; + +const ButtonBase: React.FC = ({ + variant = "default", + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +export const Button = withTooltip(ButtonBase, "block"); diff --git a/libs/@hashintel/petrinaut/src/components/code-editor.tsx b/libs/@hashintel/petrinaut/src/components/code-editor.tsx new file mode 100644 index 00000000000..2c0a62d349b --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/code-editor.tsx @@ -0,0 +1,98 @@ +import { css, cva } from "@hashintel/ds-helpers/css"; +import type { EditorProps, Monaco } from "@monaco-editor/react"; +import MonacoEditor from "@monaco-editor/react"; +import type { editor } from "monaco-editor"; +import { useCallback, useRef } from "react"; + +import { Tooltip } from "./tooltip"; + +const containerStyle = cva({ + base: { + position: "relative", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + overflow: "hidden", + }, + variants: { + isReadOnly: { + true: { + filter: "[grayscale(20%) brightness(98%)]", + cursor: "not-allowed", + }, + false: {}, + }, + }, +}); + +type CodeEditorProps = Omit & { + tooltip?: string; +}; + +/** + * Code editor component that wraps Monaco Editor. + * + * @param tooltip - Optional tooltip to show when hovering over the editor. + * In read-only mode, the tooltip also appears when attempting to edit. + */ +export const CodeEditor: React.FC = ({ + tooltip, + options, + height, + onMount, + ...props +}) => { + const isReadOnly = options?.readOnly === true; + const editorRef = useRef(null); + + const handleMount = useCallback( + (editorInstance: editor.IStandaloneCodeEditor, monaco: Monaco) => { + editorRef.current = editorInstance; + // Call the original onMount if provided + onMount?.(editorInstance, monaco); + }, + [onMount], + ); + + const editorOptions: EditorProps["options"] = { + minimap: { enabled: false }, + scrollBeyondLastLine: false, + fontSize: 12, + lineNumbers: "off", + folding: true, + glyphMargin: false, + lineDecorationsWidth: 0, + lineNumbersMinChars: 3, + padding: { top: 8, bottom: 8 }, + fixedOverflowWidgets: true, + ...options, + }; + + const editorElement = ( +
+ +
+ ); + + // Regular tooltip for non-read-only mode (if tooltip is provided) + if (tooltip) { + return ( + + {editorElement} + + ); + } + + return editorElement; +}; diff --git a/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx b/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx deleted file mode 100644 index 8590a0f1785..00000000000 --- a/libs/@hashintel/petrinaut/src/components/disabled-tooltip.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type { ReactNode } from "react"; - -import { UI_MESSAGES } from "../constants/ui-messages"; -import { Tooltip } from "./tooltip"; - -interface DisabledTooltipProps { - /** - * Whether the wrapped element is disabled. - * When true, a tooltip explaining why will be shown on hover. - */ - disabled: boolean; - /** - * The content to wrap. Should be a single element. - */ - children: ReactNode; - /** - * Optional custom message. Defaults to the standard readonly mode message. - */ - message?: string; -} - -/** - * Wraps children with an explanatory tooltip when disabled. - * - * Use this to wrap disabled form controls to explain why they're disabled - * (e.g., during simulation mode). - * - * @example - * - * - * - */ -export const DisabledTooltip: React.FC = ({ - disabled, - children, - message = UI_MESSAGES.READ_ONLY_MODE, -}) => { - if (!disabled) { - return children; - } - - return {children}; -}; diff --git a/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx new file mode 100644 index 00000000000..dac664db87d --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/hoc/with-tooltip.tsx @@ -0,0 +1,63 @@ +import type { ComponentType } from "react"; + +import { Tooltip } from "../tooltip"; + +type WithTooltipProps = { + /** Tooltip to show when hovering (useful for explaining disabled state). */ + tooltip?: string; + /** + * Display mode for the tooltip wrapper element. + * - "block": For full-width elements like inputs/selects (default) + * - "inline": For inline elements like buttons in flex containers + */ + tooltipDisplay?: "block" | "inline"; +}; + +/** + * Higher-Order Component that adds tooltip support to any component. + * + * When `tooltip` prop is provided, the component is wrapped with a Tooltip. + * When `tooltip` is undefined/empty, the component renders unwrapped. + * + * @param Component - The component to wrap + * @param defaultDisplay - Default display mode for the tooltip wrapper ("block" | "inline") + * + * @example + * ```tsx + * const MyButtonBase: React.FC<{ onClick: () => void }> = ({ onClick }) => ( + * + * ); + * + * export const MyButton = withTooltip(MyButtonBase, "inline"); + * + * // Usage: + * + * ``` + */ +export function withTooltip

( + Component: ComponentType

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

{ + const WrappedComponent: React.FC

= ({ + tooltip, + tooltipDisplay = defaultDisplay, + ...props + }) => { + const element = ; + + if (!tooltip) { + return element; + } + + return ( + + {element} + + ); + }; + + // Set display name for debugging + WrappedComponent.displayName = `withTooltip(${Component.displayName ?? Component.name})`; + + return WrappedComponent; +} diff --git a/libs/@hashintel/petrinaut/src/components/icon-button.tsx b/libs/@hashintel/petrinaut/src/components/icon-button.tsx new file mode 100644 index 00000000000..048a519f714 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/icon-button.tsx @@ -0,0 +1,115 @@ +import { cva } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const iconButtonStyle = cva({ + base: { + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "0", + border: "none", + background: "[transparent]", + borderRadius: "md.4", + cursor: "pointer", + transition: "[all 0.15s ease]", + }, + variants: { + size: { + sm: { + width: "[20px]", + height: "[20px]", + fontSize: "[14px]", + }, + md: { + width: "[24px]", + height: "[24px]", + fontSize: "[16px]", + }, + lg: { + width: "[32px]", + height: "[32px]", + fontSize: "[20px]", + }, + }, + variant: { + default: { + color: "gray.60", + _hover: { + color: "gray.80", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + }, + }, + danger: { + color: "gray.60", + _hover: { + color: "red.60", + backgroundColor: "red.10", + }, + }, + }, + isDisabled: { + true: { + opacity: "[0.5]", + cursor: "not-allowed", + _hover: { + color: "gray.60", + backgroundColor: "[transparent]", + }, + }, + false: {}, + }, + }, + defaultVariants: { + size: "md", + variant: "default", + isDisabled: false, + }, + compoundVariants: [ + { + variant: "danger", + isDisabled: true, + css: { + _hover: { + color: "gray.60", + backgroundColor: "[transparent]", + }, + }, + }, + ], +}); + +type IconButtonProps = React.ButtonHTMLAttributes & { + /** Size variant */ + size?: "sm" | "md" | "lg"; + /** Style variant */ + variant?: "default" | "danger"; + /** Icon content */ + children: React.ReactNode; + /** Accessibility label (required for icon-only buttons) */ + "aria-label": string; + /** Ref to the button element */ + ref?: React.Ref; +}; + +const IconButtonBase: React.FC = ({ + size = "md", + variant = "default", + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +export const IconButton = withTooltip(IconButtonBase, "inline"); diff --git a/libs/@hashintel/petrinaut/src/components/input.tsx b/libs/@hashintel/petrinaut/src/components/input.tsx new file mode 100644 index 00000000000..539b726f8b0 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/input.tsx @@ -0,0 +1,71 @@ +import { cva } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const inputStyle = cva({ + base: { + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.15)]", + borderRadius: "[4px]", + width: "[100%]", + boxSizing: "border-box", + }, + variants: { + isDisabled: { + true: { + backgroundColor: "[rgba(0, 0, 0, 0.02)]", + cursor: "not-allowed", + }, + false: { + backgroundColor: "[white]", + cursor: "text", + }, + }, + isMonospace: { + true: { + fontFamily: "[monospace]", + }, + false: {}, + }, + hasError: { + true: { + borderColor: "[#ef4444]", + }, + false: {}, + }, + }, + defaultVariants: { + isDisabled: false, + isMonospace: false, + hasError: false, + }, +}); + +type InputProps = Omit, "type"> & { + /** Whether to use monospace font */ + monospace?: boolean; + /** Whether the input has an error */ + hasError?: boolean; + /** Ref to the input element */ + ref?: React.Ref; +}; + +const InputBase: React.FC = ({ + monospace = false, + hasError = false, + disabled, + className, + ref, + ...props +}) => ( + +); + +export const Input = withTooltip(InputBase, "block"); diff --git a/libs/@hashintel/petrinaut/src/components/number-input.tsx b/libs/@hashintel/petrinaut/src/components/number-input.tsx new file mode 100644 index 00000000000..c364c8d8fbb --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/number-input.tsx @@ -0,0 +1,65 @@ +import { cva } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const numberInputStyle = cva({ + base: { + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.15)]", + borderRadius: "[4px]", + width: "[100%]", + boxSizing: "border-box", + fontFamily: "[monospace]", + }, + variants: { + isDisabled: { + true: { + backgroundColor: "[rgba(0, 0, 0, 0.02)]", + cursor: "not-allowed", + }, + false: { + backgroundColor: "[white]", + cursor: "text", + }, + }, + hasError: { + true: { + borderColor: "[#ef4444]", + }, + false: {}, + }, + }, + defaultVariants: { + isDisabled: false, + hasError: false, + }, +}); + +type NumberInputProps = Omit< + React.InputHTMLAttributes, + "type" +> & { + /** Whether the input has an error */ + hasError?: boolean; + /** Ref to the input element */ + ref?: React.Ref; +}; + +const NumberInputBase: React.FC = ({ + hasError = false, + disabled, + className, + ref, + ...props +}) => ( + +); + +export const NumberInput = withTooltip(NumberInputBase, "block"); diff --git a/libs/@hashintel/petrinaut/src/components/segment-group.tsx b/libs/@hashintel/petrinaut/src/components/segment-group.tsx index 1a12574fbf4..96918c7d9eb 100644 --- a/libs/@hashintel/petrinaut/src/components/segment-group.tsx +++ b/libs/@hashintel/petrinaut/src/components/segment-group.tsx @@ -1,6 +1,8 @@ import { SegmentGroup as ArkSegmentGroup } from "@ark-ui/react/segment-group"; import { cva } from "@hashintel/ds-helpers/css"; +import { withTooltip } from "./hoc/with-tooltip"; + const containerStyle = cva({ base: { display: "flex", @@ -19,9 +21,17 @@ const containerStyle = cva({ padding: "[3px]", }, }, + isDisabled: { + true: { + opacity: "[0.6]", + cursor: "not-allowed", + }, + false: {}, + }, }, defaultVariants: { size: "md", + isDisabled: false, }, }); @@ -55,7 +65,6 @@ const itemStyle = cva({ flex: "1", fontWeight: "medium", textAlign: "center", - cursor: "pointer", transition: "[all 0.2s ease]", position: "relative", zIndex: 1, @@ -77,9 +86,19 @@ const itemStyle = cva({ padding: "[1px 8px]", }, }, + isDisabled: { + true: { + cursor: "not-allowed", + pointerEvents: "none", + }, + false: { + cursor: "pointer", + }, + }, }, defaultVariants: { size: "md", + isDisabled: false, }, }); @@ -94,30 +113,40 @@ interface SegmentGroupProps { onChange: (value: string) => void; /** Size variant. Defaults to "md". */ size?: "md" | "sm"; + /** Whether the segment group is disabled. */ + disabled?: boolean; } -export const SegmentGroup: React.FC = ({ +const SegmentGroupBase: React.FC = ({ value, options, onChange, size = "md", + disabled = false, }) => { + const containerClassName = containerStyle({ size, isDisabled: disabled }); + return ( { if (details.value) { onChange(details.value); } }} > -

+
{options.map((option) => ( {option.label} @@ -128,3 +157,5 @@ export const SegmentGroup: React.FC = ({ ); }; + +export const SegmentGroup = withTooltip(SegmentGroupBase, "block"); diff --git a/libs/@hashintel/petrinaut/src/components/select.tsx b/libs/@hashintel/petrinaut/src/components/select.tsx new file mode 100644 index 00000000000..09594a7e8e4 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/select.tsx @@ -0,0 +1,53 @@ +import { cva } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const selectStyle = cva({ + base: { + fontSize: "[14px]", + padding: "[6px 8px]", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + borderRadius: "[4px]", + width: "[100%]", + boxSizing: "border-box", + }, + variants: { + isDisabled: { + true: { + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + cursor: "not-allowed", + }, + false: { + backgroundColor: "[white]", + cursor: "pointer", + }, + }, + }, + defaultVariants: { + isDisabled: false, + }, +}); + +type SelectProps = React.SelectHTMLAttributes & { + /** Ref to the select element */ + ref?: React.Ref; +}; + +const SelectBase: React.FC = ({ + disabled, + className, + children, + ref, + ...props +}) => ( + +); + +export const Select = withTooltip(SelectBase, "block"); diff --git a/libs/@hashintel/petrinaut/src/components/slider.tsx b/libs/@hashintel/petrinaut/src/components/slider.tsx new file mode 100644 index 00000000000..384519703c1 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/components/slider.tsx @@ -0,0 +1,48 @@ +import { css } from "@hashintel/ds-helpers/css"; + +import { withTooltip } from "./hoc/with-tooltip"; + +const sliderStyle = css({ + height: "[4px]", + appearance: "none", + background: "gray.30", + borderRadius: "[2px]", + outline: "none", + cursor: "pointer", + "&:disabled": { + opacity: "[0.5]", + cursor: "not-allowed", + }, + "&::-webkit-slider-thumb": { + appearance: "none", + width: "[12px]", + height: "[12px]", + borderRadius: "[50%]", + background: "blue.50", + cursor: "pointer", + }, + "&::-moz-range-thumb": { + width: "[12px]", + height: "[12px]", + borderRadius: "[50%]", + background: "blue.50", + cursor: "pointer", + border: "none", + }, +}); + +type SliderProps = Omit, "type"> & { + /** Ref to the input element */ + ref?: React.Ref; +}; + +const SliderBase: React.FC = ({ className, ref, ...props }) => ( + +); + +export const Slider = withTooltip(SliderBase, "inline"); diff --git a/libs/@hashintel/petrinaut/src/components/switch.tsx b/libs/@hashintel/petrinaut/src/components/switch.tsx index d0bb2ac4ac8..23ec285075c 100644 --- a/libs/@hashintel/petrinaut/src/components/switch.tsx +++ b/libs/@hashintel/petrinaut/src/components/switch.tsx @@ -1,58 +1,65 @@ import { Switch as ArkSwitch } from "@ark-ui/react/switch"; import { css } from "@hashintel/ds-helpers/css"; +import { withTooltip } from "./hoc/with-tooltip"; + +const controlStyle = css({ + position: "relative", + display: "inline-block", + width: "[34px]", + height: "[20px]", + borderRadius: "[10px]", + transition: "[all 0.2s ease]", + backgroundColor: "gray.40", + _checked: { + backgroundColor: "green.40", + }, + _disabled: { + cursor: "not-allowed", + }, + _enabled: { + cursor: "pointer", + }, +}); + +const thumbStyle = css({ + position: "absolute", + top: "[3px]", + left: "[3px]", + width: "[14px]", + height: "[14px]", + borderRadius: "[7px]", + backgroundColor: "[white]", + boxShadow: "[0 2px 4px rgba(0,0,0,0.2)]", + transition: "[all 0.2s ease]", + "&[data-state='checked']": { + transform: "[translateX(14px)]", + }, +}); + interface SwitchProps { checked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean; } -export const Switch: React.FC = ({ +const SwitchBase: React.FC = ({ checked, onCheckedChange, disabled = false, -}) => { - return ( - { - onCheckedChange?.(details.checked); - }} - disabled={disabled} - > - - - - - - ); -}; +}) => ( + { + onCheckedChange?.(details.checked); + }} + disabled={disabled} + > + + + + + +); + +export const Switch = withTooltip(SwitchBase, "inline"); diff --git a/libs/@hashintel/petrinaut/src/components/tooltip.tsx b/libs/@hashintel/petrinaut/src/components/tooltip.tsx index f22f9c38cd2..880bb925ce6 100644 --- a/libs/@hashintel/petrinaut/src/components/tooltip.tsx +++ b/libs/@hashintel/petrinaut/src/components/tooltip.tsx @@ -1,5 +1,6 @@ +import { ark } from "@ark-ui/react/factory"; import { Tooltip as ArkTooltip } from "@ark-ui/react/tooltip"; -import { css } from "@hashintel/ds-helpers/css"; +import { css, cva, cx } from "@hashintel/ds-helpers/css"; import type { SvgIconProps } from "@mui/material"; import { SvgIcon, Tooltip as MuiTooltip } from "@mui/material"; import type { FunctionComponent, ReactNode } from "react"; @@ -14,19 +15,69 @@ const tooltipContentStyle = css({ padding: "[6px 10px]", }); +const triggerWrapperStyle = cva({ + variants: { + display: { + /** For block-level elements like inputs, selects - takes full width */ + block: { + display: "block", + }, + /** For inline elements like buttons in flex containers */ + inline: { + display: "inline-block", + }, + }, + }, + defaultVariants: { + display: "block", + }, +}); + interface TooltipProps { - content: string; + /** + * The tooltip content. When empty/undefined, children are rendered without tooltip wrapper. + */ + content?: string; children: ReactNode; + /** + * Display mode for the wrapper element. + * - "block": For full-width elements like inputs/selects (default) + * - "inline": For inline elements like buttons in flex containers + */ + display?: "block" | "inline"; + /** + * Optional className to apply to the trigger wrapper element. + */ + className?: string; } -export const Tooltip: React.FC = ({ content, children }) => { +/** + * Tooltip component that wraps children and shows a tooltip on hover. + * + * Uses a wrapper element to capture pointer events, enabling tooltips on disabled elements. + * Set `display="inline"` when wrapping inline elements like buttons. + */ +export const Tooltip: React.FC = ({ + content, + children, + display = "block", + className, +}) => { + if (!content) { + return children; + } + return ( - {children} + + + {children} + + {content} diff --git a/libs/@hashintel/petrinaut/src/constants/ui-messages.ts b/libs/@hashintel/petrinaut/src/constants/ui-messages.ts index 4123bcf09ed..fb01c021686 100644 --- a/libs/@hashintel/petrinaut/src/constants/ui-messages.ts +++ b/libs/@hashintel/petrinaut/src/constants/ui-messages.ts @@ -3,5 +3,6 @@ */ export const UI_MESSAGES = { AI_FEATURE_COMING_SOON: "AI generation feature coming soon", + DYNAMICS_REQUIRES_TYPE: "Select a token type to enable dynamics", READ_ONLY_MODE: "Editing disabled while in simulation mode", } as const; diff --git a/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts index 09c87863f16..d911f11e673 100644 --- a/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts +++ b/libs/@hashintel/petrinaut/src/state/use-is-read-only.ts @@ -8,17 +8,19 @@ import { EditorContext } from "./editor-context"; * * The editor is read-only when: * 1. The global mode is "simulate" (user has switched to simulation mode), OR - * 2. A simulation is currently running or paused (has been initialized) + * 2. A simulation is currently running, paused, or complete * * When read-only, structural changes to the SDCPN (places, transitions, arcs, etc.) - * are prevented to maintain consistency with the running simulation. + * are prevented to maintain consistency with the simulation. */ export const useIsReadOnly = (): boolean => { const { globalMode } = use(EditorContext); const { state: simulationState } = use(SimulationContext); const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + simulationState === "Running" || + simulationState === "Paused" || + simulationState === "Complete"; const isReadOnly = globalMode === "simulate" || isSimulationActive; return isReadOnly; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx index 87e7a7a0917..c9c78f0b1c0 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/toolbar-button.tsx @@ -87,7 +87,7 @@ export const ToolbarButton: React.FC = ({ }; return ( - + - + {showTypeDropdown && !isReadOnly && (
- {types.map((type) => ( - - ))} + Create a type first +
+ ) : ( + types.map((type) => ( + + )) + )}
)}
@@ -449,20 +407,15 @@ export const DifferentialEquationProperties: React.FC< you want to continue?
- - +
@@ -503,7 +456,10 @@ export const DifferentialEquationProperties: React.FC< { id: "generate-ai", label: ( - +
Generate with AI @@ -519,37 +475,22 @@ export const DifferentialEquationProperties: React.FC< /> )}
- -
- { - updateDifferentialEquation( - differentialEquation.id, - (existingEquation) => { - existingEquation.code = newCode ?? ""; - }, - ); - }} - path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`} - theme="vs-light" - options={{ - minimap: { enabled: false }, - scrollBeyondLastLine: false, - fontSize: 12, - lineNumbers: "off", - folding: true, - glyphMargin: false, - lineDecorationsWidth: 0, - lineNumbersMinChars: 3, - padding: { top: 8, bottom: 8 }, - fixedOverflowWidgets: true, - readOnly: isReadOnly, - }} - /> -
-
+ { + updateDifferentialEquation( + differentialEquation.id, + (existingEquation) => { + existingEquation.code = newCode ?? ""; + }, + ); + }} + path={`inmemory://sdcpn/differential-equations/${differentialEquation.id}.ts`} + options={{ readOnly: isReadOnly }} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> ); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx index 231415dc6a8..a6f49c2b7d6 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/parameter-properties.tsx @@ -1,6 +1,7 @@ -import { css, cva } from "@hashintel/ds-helpers/css"; +import { css } from "@hashintel/ds-helpers/css"; -import { DisabledTooltip } from "../../../../components/disabled-tooltip"; +import { Input } from "../../../../components/input"; +import { UI_MESSAGES } from "../../../../constants/ui-messages"; import type { Parameter } from "../../../../core/types/sdcpn"; import { useIsReadOnly } from "../../../../state/use-is-read-only"; @@ -22,38 +23,6 @@ const fieldLabelStyle = css({ marginBottom: "[4px]", }); -const inputStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.15)]", - borderRadius: "[4px]", - width: "[100%]", - }, - variants: { - isDisabled: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.02)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - isMonospace: { - true: { - fontFamily: "[monospace]", - }, - false: {}, - }, - }, - defaultVariants: { - isDisabled: false, - isMonospace: false, - }, -}); - /** * Slugifies a string to a valid JavaScript identifier. * - Converts to lowercase @@ -142,30 +111,25 @@ export const ParameterProperties: React.FC = ({ {/* Name field */}
Name
- - - +
{/* Variable Name field */}
Variable Name
- - - +
{/* Type selector - hidden for now as internal code relies on "real" type */} @@ -173,15 +137,13 @@ export const ParameterProperties: React.FC = ({ {/* Default Value field */}
Default Value
- - - +
); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx index 62d8875fcac..9fa8709d250 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/place-properties.tsx @@ -1,5 +1,5 @@ /* eslint-disable id-length */ -import { css, cva } from "@hashintel/ds-helpers/css"; +import { css } from "@hashintel/ds-helpers/css"; import MonacoEditor from "@monaco-editor/react"; import { use, useEffect, useMemo, useRef, useState } from "react"; import { @@ -9,8 +9,11 @@ import { TbTrash, } from "react-icons/tb"; -import { DisabledTooltip } from "../../../../components/disabled-tooltip"; +import { Button } from "../../../../components/button"; +import { IconButton } from "../../../../components/icon-button"; +import { Input } from "../../../../components/input"; import { Menu } from "../../../../components/menu"; +import { Select } from "../../../../components/select"; import type { SubView } from "../../../../components/sub-view/types"; import { FixedHeightSubViewsContainer } from "../../../../components/sub-view/vertical-sub-views-container"; import { Switch } from "../../../../components/switch"; @@ -61,24 +64,6 @@ const headerTitleStyle = css({ fontSize: "[16px]", }); -const deleteButtonStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[24px]", - height: "[24px]", - padding: "0", - border: "none", - background: "[transparent]", - cursor: "pointer", - color: "gray.60", - borderRadius: "md.4", - _hover: { - color: "red.60", - backgroundColor: "red.10", - }, -}); - const fieldLabelStyle = css({ fontWeight: "medium", fontSize: "[12px]", @@ -93,92 +78,16 @@ const fieldLabelWithTooltipStyle = css({ alignItems: "center", }); -const inputStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - borderRadius: "[4px]", - width: "[100%]", - boxSizing: "border-box", - }, - variants: { - isReadOnly: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - hasError: { - true: { - border: "[1px solid #ef4444]", - }, - false: { - border: "[1px solid rgba(0, 0, 0, 0.1)]", - }, - }, - }, - defaultVariants: { - isReadOnly: false, - hasError: false, - }, -}); - const errorMessageStyle = css({ fontSize: "[12px]", color: "[#ef4444]", marginTop: "[4px]", }); -const selectStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - borderRadius: "[4px]", - width: "[100%]", - boxSizing: "border-box", - }, - variants: { - isReadOnly: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "pointer", - }, - }, - hasMarginBottom: { - true: { - marginBottom: "[8px]", - }, - false: {}, - }, - }, -}); - const jumpButtonContainerStyle = css({ textAlign: "right", }); -const jumpButtonStyle = css({ - fontSize: "[12px]", - padding: "[4px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.2)]", - borderRadius: "[4px]", - backgroundColor: "[white]", - cursor: "pointer", - color: "[#333]", - display: "inline-flex", - alignItems: "center", - gap: "[6px]", -}); - const jumpIconStyle = css({ fontSize: "[14px]", }); @@ -384,50 +293,48 @@ export const PlaceProperties: React.FC = ({
Place
- - - + { + if ( + // eslint-disable-next-line no-alert + window.confirm( + `Are you sure you want to delete "${place.name}"? All arcs connected to this place will also be removed.`, + ) + ) { + removePlace(place.id); + } + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"} + > + +
Name
- - { - setNameInputValue(event.target.value); - // Clear error when user starts typing - if (nameError) { - setNameError(null); - } - }} - onFocus={() => setIsNameInputFocused(true)} - onBlur={() => { - setIsNameInputFocused(false); - handleNameBlur(); - }} - disabled={isReadOnly} - className={inputStyle({ isReadOnly, hasError: !!nameError })} - /> - + { + setNameInputValue(event.target.value); + // Clear error when user starts typing + if (nameError) { + setNameError(null); + } + }} + onFocus={() => setIsNameInputFocused(true)} + onBlur={() => { + setIsNameInputFocused(false); + handleNameBlur(); + }} + disabled={isReadOnly} + hasError={!!nameError} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> {nameError &&
{nameError}
}
@@ -442,47 +349,41 @@ export const PlaceProperties: React.FC = ({ } Tokens in places don't have to carry data, but they need one to enable dynamics (token data changing over time when in a place).`} /> - - - + {place.colorId && (
- +
)} @@ -490,17 +391,22 @@ export const PlaceProperties: React.FC = ({
- - { - updatePlace(place.id, (existingPlace) => { - existingPlace.dynamicsEnabled = checked; - }); - }} - /> - + { + updatePlace(place.id, (existingPlace) => { + existingPlace.dynamicsEnabled = checked; + }); + }} + />
Dynamics @@ -523,40 +429,37 @@ export const PlaceProperties: React.FC = ({ availableDiffEqs.length > 0 && (
Differential Equation
- - - + {place.differentialEquationId && (
- +
)}
@@ -569,6 +472,8 @@ export const PlaceProperties: React.FC = ({
{ if (checked) { // Turning on: use saved code if available, otherwise default @@ -627,7 +532,10 @@ export const PlaceProperties: React.FC = ({ { id: "generate-ai", label: ( - +
Generate with AI diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx index 55c947d3b4b..d68cb37ac3c 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/sortable-arc-item.tsx @@ -4,6 +4,8 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { MdDragIndicator } from "react-icons/md"; import { TbTrash } from "react-icons/tb"; +import { IconButton } from "../../../../components/icon-button"; +import { NumberInput } from "../../../../components/number-input"; import { FEATURE_FLAGS } from "../../../../feature-flags"; const containerStyle = css({ @@ -60,46 +62,10 @@ const weightLabelStyle = css({ fontWeight: "medium", }); -const weightInputStyle = cva({ - base: { - width: "[60px]", - fontSize: "[14px]", - padding: "[4px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - borderRadius: "[4px]", - boxSizing: "border-box", - }, - variants: { - isDisabled: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - }, -}); - -const deleteButtonStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[24px]", - height: "[24px]", - padding: "0", - border: "none", - background: "[transparent]", - cursor: "pointer", - color: "gray.60", - flexShrink: 0, - borderRadius: "md.4", - _hover: { - color: "red.60", - backgroundColor: "red.10", - }, +const weightInputStyle = css({ + width: "[60px]", + fontSize: "[14px]", + padding: "[4px 8px]", }); /** @@ -110,6 +76,8 @@ interface SortableArcItemProps { placeName: string; weight: number; disabled?: boolean; + /** Tooltip to show when disabled (e.g., for read-only mode) */ + tooltip?: string; onWeightChange: (weight: number) => void; onDelete?: () => void; } @@ -119,6 +87,7 @@ export const SortableArcItem: React.FC = ({ placeName, weight, disabled = false, + tooltip, onWeightChange, onDelete, }) => { @@ -151,25 +120,25 @@ export const SortableArcItem: React.FC = ({
{placeName}
weight - { const newWeight = Number.parseInt(event.target.value, 10); if (!Number.isNaN(newWeight) && newWeight >= 1) { onWeightChange(newWeight); } }} - className={weightInputStyle({ isDisabled: disabled })} + className={weightInputStyle} />
{onDelete && !disabled && ( - + )}
); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx index 83f4ce1dd97..22201b754ee 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/transition-properties.tsx @@ -15,12 +15,13 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { css, cva } from "@hashintel/ds-helpers/css"; -import MonacoEditor from "@monaco-editor/react"; +import { css } from "@hashintel/ds-helpers/css"; import { use } from "react"; import { TbDotsVertical, TbSparkles, TbTrash } from "react-icons/tb"; -import { DisabledTooltip } from "../../../../components/disabled-tooltip"; +import { CodeEditor } from "../../../../components/code-editor"; +import { IconButton } from "../../../../components/icon-button"; +import { Input } from "../../../../components/input"; import { Menu } from "../../../../components/menu"; import { SegmentGroup } from "../../../../components/segment-group"; import { InfoIconTooltip, Tooltip } from "../../../../components/tooltip"; @@ -53,53 +54,12 @@ const headerTitleStyle = css({ fontSize: "[16px]", }); -const deleteButtonStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "center", - width: "[24px]", - height: "[24px]", - padding: "0", - border: "none", - background: "[transparent]", - cursor: "pointer", - color: "gray.60", - borderRadius: "md.4", - _hover: { - color: "red.60", - backgroundColor: "red.10", - }, -}); - const fieldLabelStyle = css({ fontWeight: "medium", fontSize: "[12px]", marginBottom: "[4px]", }); -const inputStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - borderRadius: "[4px]", - width: "[100%]", - boxSizing: "border-box", - }, - variants: { - isReadOnly: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - }, -}); - const sectionContainerStyle = css({ marginTop: "[20px]", }); @@ -115,22 +75,8 @@ const arcListContainerStyle = css({ overflow: "hidden", }); -const segmentGroupWrapperStyle = cva({ - base: { - marginTop: "[8px]", - }, - variants: { - isReadOnly: { - true: { - opacity: "[0.6]", - pointerEvents: "none", - }, - false: { - opacity: "[1]", - pointerEvents: "auto", - }, - }, - }, +const segmentGroupContainerStyle = css({ + marginTop: "[8px]", }); const infoBoxStyle = css({ @@ -166,29 +112,6 @@ const menuButtonStyle = css({ color: "[rgba(0, 0, 0, 0.6)]", }); -const editorContainerStyle = cva({ - base: { - border: "[1px solid rgba(0, 0, 0, 0.1)]", - borderRadius: "[4px]", - overflow: "hidden", - }, - variants: { - size: { - lambda: { height: "[340px]" }, - kernel: { height: "[400px]" }, - }, - isReadOnly: { - true: { - filter: "[grayscale(20%) brightness(98%)]", - pointerEvents: "none", - }, - false: { - pointerEvents: "auto", - }, - }, - }, -}); - const aiMenuItemStyle = css({ display: "flex", alignItems: "center", @@ -342,42 +265,39 @@ export const TransitionProperties: React.FC = ({
Transition
- - - + { + if ( + // eslint-disable-next-line no-alert + window.confirm( + `Are you sure you want to delete "${transition.name}"? All arcs connected to this transition will also be removed.`, + ) + ) { + removeTransition(transition.id); + } + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : "Delete"} + > + +
Name
- - { - updateTransition(transition.id, (existingTransition) => { - existingTransition.name = event.target.value; - }); - }} - disabled={isReadOnly} - className={inputStyle({ isReadOnly })} - /> - + { + updateTransition(transition.id, (existingTransition) => { + existingTransition.name = event.target.value; + }); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
@@ -408,6 +328,9 @@ export const TransitionProperties: React.FC = ({ placeName={place?.name ?? arc.placeId} weight={arc.weight} disabled={isReadOnly} + tooltip={ + isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined + } onWeightChange={(weight) => { onArcWeightUpdate( transition.id, @@ -454,6 +377,9 @@ export const TransitionProperties: React.FC = ({ placeName={place?.name ?? arc.placeId} weight={arc.weight} disabled={isReadOnly} + tooltip={ + isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined + } onWeightChange={(weight) => { onArcWeightUpdate( transition.id, @@ -477,24 +403,24 @@ export const TransitionProperties: React.FC = ({ Firing time
- -
- { - updateTransition(transition.id, (existingTransition) => { - existingTransition.lambdaType = value as - | "predicate" - | "stochastic"; - }); - }} - /> -
-
+
+ { + updateTransition(transition.id, (existingTransition) => { + existingTransition.lambdaType = value as + | "predicate" + | "stochastic"; + }); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> +
@@ -534,7 +460,10 @@ export const TransitionProperties: React.FC = ({ { id: "generate-ai", label: ( - +
Generate with AI @@ -550,37 +479,22 @@ export const TransitionProperties: React.FC = ({ /> )}
- -
- `${a.placeId}:${a.weight}`) - .join("-")}`} - language="typescript" - value={transition.lambdaCode || ""} - path={`inmemory://sdcpn/transitions/${transition.id}/lambda.ts`} - onChange={(value) => { - updateTransition(transition.id, (existingTransition) => { - existingTransition.lambdaCode = value ?? ""; - }); - }} - theme="vs-light" - options={{ - minimap: { enabled: false }, - scrollBeyondLastLine: false, - fontSize: 12, - lineNumbers: "off", - folding: true, - glyphMargin: false, - lineDecorationsWidth: 0, - lineNumbersMinChars: 3, - padding: { top: 8, bottom: 8 }, - readOnly: isReadOnly, - fixedOverflowWidgets: true, - }} - /> -
-
+ `${a.placeId}:${a.weight}`) + .join("-")}`} + language="typescript" + value={transition.lambdaCode || ""} + path={`inmemory://sdcpn/transitions/${transition.id}/lambda.ts`} + height={340} + onChange={(value) => { + updateTransition(transition.id, (existingTransition) => { + existingTransition.lambdaCode = value ?? ""; + }); + }} + options={{ readOnly: isReadOnly }} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
{/* Only show Transition Results if at least one output place has a type */} @@ -649,7 +563,10 @@ export const TransitionProperties: React.FC = ({ { id: "generate-ai", label: ( - +
Generate with AI @@ -665,41 +582,24 @@ export const TransitionProperties: React.FC = ({ /> )}
- -
- `${a.placeId}:${a.weight}`) - .join("-")}-${transition.outputArcs - .map((a) => `${a.placeId}:${a.weight}`) - .join("-")}`} - language="typescript" - value={transition.transitionKernelCode || ""} - path={`inmemory://sdcpn/transitions/${transition.id}/transition-kernel.ts`} - onChange={(value) => { - updateTransition(transition.id, (existingTransition) => { - existingTransition.transitionKernelCode = value ?? ""; - }); - }} - theme="vs-light" - options={{ - minimap: { enabled: false }, - scrollBeyondLastLine: false, - fontSize: 12, - lineNumbers: "off", - folding: true, - glyphMargin: false, - lineDecorationsWidth: 0, - lineNumbersMinChars: 3, - padding: { top: 8, bottom: 8 }, - readOnly: isReadOnly, - fixedOverflowWidgets: true, - }} - /> -
-
+ `${a.placeId}:${a.weight}`) + .join("-")}-${transition.outputArcs + .map((a) => `${a.placeId}:${a.weight}`) + .join("-")}`} + language="typescript" + value={transition.transitionKernelCode || ""} + path={`inmemory://sdcpn/transitions/${transition.id}/transition-kernel.ts`} + height={400} + onChange={(value) => { + updateTransition(transition.id, (existingTransition) => { + existingTransition.transitionKernelCode = value ?? ""; + }); + }} + options={{ readOnly: isReadOnly }} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
) : (
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx index aa96b289689..841431dc33f 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/PropertiesPanel/type-properties.tsx @@ -2,7 +2,10 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; -import { DisabledTooltip } from "../../../../components/disabled-tooltip"; +import { Button } from "../../../../components/button"; +import { Input } from "../../../../components/input"; +import { Tooltip } from "../../../../components/tooltip"; +import { UI_MESSAGES } from "../../../../constants/ui-messages"; import type { Color } from "../../../../core/types/sdcpn"; import { useIsReadOnly } from "../../../../state/use-is-read-only"; import { ColorSelect } from "./color-select"; @@ -25,29 +28,6 @@ const fieldLabelStyle = css({ marginBottom: "[4px]", }); -const inputStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - borderRadius: "[4px]", - width: "[100%]", - boxSizing: "border-box", - }, - variants: { - isDisabled: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - }, -}); - const dimensionsHeaderStyle = css({ display: "flex", alignItems: "center", @@ -71,22 +51,16 @@ const addDimensionButtonStyle = cva({ base: { fontSize: "[16px]", padding: "[2px 8px]", - borderRadius: "[4px]", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - fontWeight: "semibold", - cursor: "pointer", }, variants: { isDisabled: { true: { backgroundColor: "[rgba(0, 0, 0, 0.05)]", color: "[#999]", - cursor: "not-allowed", }, false: { backgroundColor: "[rgba(59, 130, 246, 0.1)]", color: "[#3b82f6]", - cursor: "pointer", }, }, }, @@ -179,54 +153,38 @@ const indexChipStyle = css({ flexShrink: 0, }); -const dimensionNameInputStyle = cva({ +const dimensionNameInputStyle = css({ + fontSize: "[13px]", + padding: "[5px 8px]", + flex: "1", +}); + +const deleteDimensionButtonStyle = cva({ base: { - fontSize: "[13px]", - padding: "[5px 8px]", - border: "[1px solid rgba(0, 0, 0, 0.15)]", + fontSize: "[16px]", + width: "[28px]", + height: "[28px]", borderRadius: "[3px]", - flex: "1", + fontWeight: "semibold", + lineHeight: "[1]", + flexShrink: 0, }, variants: { isDisabled: { true: { - backgroundColor: "[rgba(0, 0, 0, 0.02)]", - cursor: "not-allowed", + border: "[1px solid rgba(0, 0, 0, 0.1)]", + backgroundColor: "[rgba(0, 0, 0, 0.05)]", + color: "[#999]", }, false: { - backgroundColor: "[white]", - cursor: "text", + border: "[1px solid rgba(239, 68, 68, 0.2)]", + backgroundColor: "[rgba(239, 68, 68, 0.08)]", + color: "[#ef4444]", }, }, }, }); -const deleteDimensionButtonStyle = css({ - fontSize: "[16px]", - width: "[28px]", - height: "[28px]", - borderRadius: "[3px]", - border: "[1px solid rgba(239, 68, 68, 0.2)]", - backgroundColor: "[rgba(239, 68, 68, 0.08)]", - color: "[#ef4444]", - cursor: "pointer", - fontWeight: "semibold", - lineHeight: "[1]", - transition: "[all 0.15s ease]", - flexShrink: 0, - display: "flex", - alignItems: "center", - justifyContent: "center", - _hover: { - backgroundColor: "[rgba(239, 68, 68, 0.15)]", - }, - _disabled: { - backgroundColor: "[rgba(0, 0, 0, 0.02)]", - color: "[#ccc]", - cursor: "not-allowed", - }, -}); - // --- Helpers --- /** @@ -403,24 +361,21 @@ export const TypeProperties: React.FC = ({
Name
- - { - updateType(type.id, (existingType) => { - existingType.name = event.target.value; - }); - }} - disabled={isDisabled} - className={inputStyle({ isDisabled })} - /> - + { + updateType(type.id, (existingType) => { + existingType.name = event.target.value; + }); + }} + disabled={isDisabled} + tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined} + />
Color
- + { @@ -430,7 +385,7 @@ export const TypeProperties: React.FC = ({ }} disabled={isDisabled} /> - +
{/* Dimensions Section - Editable with drag-to-reorder */} @@ -440,17 +395,15 @@ export const TypeProperties: React.FC = ({ Dimensions (order matters)
- - - +
{type.elements.length === 0 ? ( @@ -489,42 +442,40 @@ export const TypeProperties: React.FC = ({
{index}
{/* Name input */} - - { - handleUpdateElementName( - element.elementId, - event.target.value, - ); - }} - onBlur={(event) => { - handleBlurElementName( - element.elementId, - event.target.value, - ); - }} - disabled={isDisabled} - placeholder="dimension_name" - className={dimensionNameInputStyle({ isDisabled })} - /> - + { + handleUpdateElementName( + element.elementId, + event.target.value, + ); + }} + onBlur={(event) => { + handleBlurElementName( + element.elementId, + event.target.value, + ); + }} + disabled={isDisabled} + placeholder="dimension_name" + className={dimensionNameInputStyle} + tooltip={isDisabled ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> {/* Delete button */} - - - +
))} diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx index e9f152fc316..f6963007cd9 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/differential-equations-list.tsx @@ -3,10 +3,12 @@ import { use } from "react"; import { v4 as uuidv4 } from "uuid"; import type { SubView } from "../../../components/sub-view/types"; +import { Tooltip } from "../../../components/tooltip"; +import { UI_MESSAGES } from "../../../constants/ui-messages"; import { DEFAULT_DIFFERENTIAL_EQUATION_CODE } from "../../../core/default-codes"; -import { SimulationContext } from "../../../simulation/context"; import { EditorContext } from "../../../state/editor-context"; import { SDCPNContext } from "../../../state/sdcpn-context"; +import { useIsReadOnly } from "../../../state/use-is-read-only"; const listContainerStyle = css({ display: "flex", @@ -120,10 +122,7 @@ const DifferentialEquationsSectionContent: React.FC = () => { const { selectedResourceId, setSelectedResourceId } = use(EditorContext); - // Check if simulation is running or paused - const { state: simulationState } = use(SimulationContext); - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = useIsReadOnly(); return (
@@ -155,24 +154,29 @@ const DifferentialEquationsSectionContent: React.FC = () => {
{eq.name}
- + +
); })} @@ -193,31 +197,33 @@ const DifferentialEquationsSectionHeaderAction: React.FC = () => { } = use(SDCPNContext); const { setSelectedResourceId } = use(EditorContext); - // Check if simulation is running or paused - const { state: simulationState } = use(SimulationContext); - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = useIsReadOnly(); return ( - + +
); }; diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx index 7de447c256a..1fbf6f543eb 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/parameters-list.tsx @@ -3,9 +3,12 @@ import { use } from "react"; import { v4 as uuidv4 } from "uuid"; import type { SubView } from "../../../components/sub-view/types"; +import { Tooltip } from "../../../components/tooltip"; +import { UI_MESSAGES } from "../../../constants/ui-messages"; import { SimulationContext } from "../../../simulation/context"; import { EditorContext } from "../../../state/editor-context"; import { SDCPNContext } from "../../../state/sdcpn-context"; +import { useIsReadOnly } from "../../../state/use-is-read-only"; const addButtonStyle = css({ display: "flex", @@ -138,12 +141,9 @@ const ParametersHeaderAction: React.FC = () => { petriNetDefinition: { parameters }, addParameter, } = use(SDCPNContext); - const { globalMode, setSelectedResourceId } = use(EditorContext); - const { state: simulationState } = use(SimulationContext); + const { setSelectedResourceId } = use(EditorContext); - const isSimulationMode = globalMode === "simulate"; - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = useIsReadOnly(); const handleAddParameter = () => { const name = `param${parameters.length + 1}`; @@ -158,21 +158,21 @@ const ParametersHeaderAction: React.FC = () => { setSelectedResourceId(id); }; - // Don't show add button in simulation mode - if (isSimulationMode) { - return null; - } - return ( - + +
); }; @@ -192,14 +192,11 @@ const ParametersList: React.FC = () => { setParameterValue, } = use(SimulationContext); + const isReadOnly = useIsReadOnly(); const isSimulationNotRun = globalMode === "simulate" && simulationState === "NotRun"; const isSimulationMode = globalMode === "simulate"; - // Check if simulation is running or paused - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - return (
@@ -250,22 +247,29 @@ const ParametersList: React.FC = () => { className={inputStyle} /> ) : ( - + + )}
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx index 60f71c98329..eaaf74ae49b 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/place-initial-state.tsx @@ -1,38 +1,14 @@ -import { css, cva } from "@hashintel/ds-helpers/css"; +import { css } from "@hashintel/ds-helpers/css"; import { use } from "react"; import { TbTrash } from "react-icons/tb"; +import { NumberInput } from "../../../components/number-input"; import type { SubView } from "../../../components/sub-view/types"; +import { UI_MESSAGES } from "../../../constants/ui-messages"; import { SimulationContext } from "../../../simulation/context"; import { InitialStateEditor } from "../panels/PropertiesPanel/initial-state-editor"; import { usePlacePropertiesContext } from "../panels/PropertiesPanel/place-properties-context"; -const inputStyle = cva({ - base: { - fontSize: "[14px]", - padding: "[6px 8px]", - borderRadius: "[4px]", - width: "[100%]", - boxSizing: "border-box", - border: "[1px solid rgba(0, 0, 0, 0.1)]", - }, - variants: { - isDisabled: { - true: { - backgroundColor: "[rgba(0, 0, 0, 0.05)]", - cursor: "not-allowed", - }, - false: { - backgroundColor: "[white]", - cursor: "text", - }, - }, - }, - defaultVariants: { - isDisabled: false, - }, -}); - const fieldLabelStyle = css({ fontWeight: "medium", fontSize: "[12px]", @@ -127,10 +103,9 @@ const PlaceInitialStateContent: React.FC = () => { return (
Token count
- { const count = Math.max( @@ -143,7 +118,7 @@ const PlaceInitialStateContent: React.FC = () => { }); }} disabled={hasSimulationFrames} - className={inputStyle({ isDisabled: hasSimulationFrames })} + tooltip={hasSimulationFrames ? UI_MESSAGES.READ_ONLY_MODE : undefined} />
); diff --git a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx index 6adf4dd58b4..a53d0046f1e 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/subviews/types-list.tsx @@ -2,9 +2,11 @@ import { css, cva } from "@hashintel/ds-helpers/css"; import { use } from "react"; import type { SubView } from "../../../components/sub-view/types"; -import { SimulationContext } from "../../../simulation/context"; +import { Tooltip } from "../../../components/tooltip"; +import { UI_MESSAGES } from "../../../constants/ui-messages"; import { EditorContext } from "../../../state/editor-context"; import { SDCPNContext } from "../../../state/sdcpn-context"; +import { useIsReadOnly } from "../../../state/use-is-read-only"; const listContainerStyle = css({ display: "flex", @@ -171,10 +173,7 @@ const TypesSectionContent: React.FC = () => { const { selectedResourceId, setSelectedResourceId } = use(EditorContext); - // Check if simulation is running or paused - const { state: simulationState } = use(SimulationContext); - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = useIsReadOnly(); return (
@@ -208,24 +207,29 @@ const TypesSectionContent: React.FC = () => { style={{ backgroundColor: type.displayColor }} /> {type.name} - + +
); })} @@ -245,41 +249,43 @@ const TypesSectionHeaderAction: React.FC = () => { addType, } = use(SDCPNContext); - // Check if simulation is running or paused - const { state: simulationState } = use(SimulationContext); - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; + const isReadOnly = useIsReadOnly(); return ( - + + ); }; diff --git a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx index 1ab40da6c44..f0f7fb4fdb8 100644 --- a/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx +++ b/libs/@hashintel/petrinaut/src/views/SDCPN/sdcpn-view.tsx @@ -10,9 +10,9 @@ import { DEFAULT_TRANSITION_KERNEL_CODE, generateDefaultLambdaCode, } from "../../core/default-codes"; -import { SimulationContext } from "../../simulation/context"; import { EditorContext } from "../../state/editor-context"; import { SDCPNContext } from "../../state/sdcpn-context"; +import { useIsReadOnly } from "../../state/use-is-read-only"; import { Arc } from "./components/arc"; import { PlaceNode } from "./components/place-node"; import { TransitionNode } from "./components/transition-node"; @@ -65,7 +65,6 @@ export const SDCPNView: React.FC = () => { } = use(SDCPNContext); const { - globalMode: mode, editionMode, setEditionMode, selectedItemIds, @@ -74,8 +73,6 @@ export const SDCPNView: React.FC = () => { clearSelection, } = use(EditorContext); - const { state: simulationState } = use(SimulationContext); - // Hook for applying node changes const applyNodeChanges = useApplyNodeChanges(); @@ -87,10 +84,9 @@ export const SDCPNView: React.FC = () => { reactFlowInstance?.fitView({ padding: 0.4, minZoom: 0.4, maxZoom: 1.1 }); }, [reactFlowInstance, petriNetId]); - // Readonly if in simulate mode, simulation is running/paused, or readonly has been provided by external consumer. - const isSimulationActive = - simulationState === "Running" || simulationState === "Paused"; - const isReadonly = mode === "simulate" || isSimulationActive || readonly; + // Readonly if simulation mode or readonly has been provided by external consumer. + const isSimulationReadOnly = useIsReadOnly(); + const isReadonly = isSimulationReadOnly || readonly; function isValidConnection(connection: Connection) { const sourceNode = nodes.find((node) => node.id === connection.source); @@ -307,7 +303,7 @@ export const SDCPNView: React.FC = () => { onKeyDown={({ key }) => { // Quick-and-dirty way to delete selected items with keyboard // with two different keys (Delete and Backspace), not possible with ReactFlow `deleteKeyCode` prop - if (key === "Delete" || key === "Backspace") { + if ((key === "Delete" || key === "Backspace") && !isReadonly) { setSelectedResourceId(null); clearSelection(); deleteItemsByIds(selectedItemIds);