Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2b7d4e3
Everything always available in BottomBar
kube Dec 15, 2025
dbd340d
Extract styles in SimulationControls
kube Dec 15, 2025
8084653
Disable SimulateMode in ModeSelector for now
kube Dec 15, 2025
de12b66
Extract styles in LeftSidebar and make all sections always visible
kube Dec 15, 2025
381bbdd
Extract styles in PlaceNode
kube Dec 17, 2025
b14cf14
Make PlaceNode token count bigger
kube Dec 17, 2025
99e93dc
Make Simulation available in Edit Mode, and disable Simulate tab for now
kube Dec 17, 2025
6b81da4
Fix BrokenMachines example
kube Dec 17, 2025
e52e6af
Wording
kube Dec 17, 2025
e721aa9
Create GlassPanel component
kube Dec 17, 2025
93baa8c
Centralize Resizable in GlassPanel
kube Dec 17, 2025
fba89ec
Update GlassPanel style
kube Dec 17, 2025
73f6dbf
Centralize UI Constants
kube Dec 18, 2025
bf5f347
Better resizer handle
kube Dec 18, 2025
079fa5b
Move RESIZE_HANDLE constants in UI constants
kube Dec 18, 2025
296b2d8
Make tabs a bit cleaner
kube Dec 18, 2025
160da9a
Move activeBottomPanel to EditorStore
kube Dec 18, 2025
7559909
Rename DiagnosticsPanel to BottomPanel
kube Dec 18, 2025
ee73389
Use Play/Pause/Stop icons
kube Dec 18, 2025
3465b28
Open Diagnostics when clicking on Run Simulation when errors disable it
kube Dec 18, 2025
2416ef8
Remove tooltip from Global Parameters panel, just show text
kube Dec 18, 2025
3e0f7e0
Cleaner BottomPanel
kube Dec 18, 2025
365087c
Remove Simulation State from Simulation Settings
kube Dec 18, 2025
8b4cbab
BottomPanel styles
kube Dec 18, 2025
68ab8ef
Add close button to BottomPanel
kube Dec 18, 2025
8d28fab
Diagnostics Indicator opens Diagnostics in BottomPanel, with Tooltip
kube Dec 18, 2025
914d97b
Add Toggle Panel button in BottomBar
kube Dec 18, 2025
1794c8d
Hide Place/Transition buttons in BottomBar when Simulation is running
kube Dec 18, 2025
6e01e42
SDCPNView readonly when Simulation is running
kube Dec 18, 2025
c35739e
Make Token Types, Differential Equations and Global Parameters readon…
kube Dec 18, 2025
d7d27dc
Make Diagnostics expanded by default
kube Dec 18, 2025
8afa254
Toolbar refinement + updates from demo review
kube Dec 19, 2025
a314960
Adjust bezelWidth for current BottomBar height
kube Dec 19, 2025
b41dd58
Swap Parameters and Computation sections
kube Dec 19, 2025
ca274c9
Add tooltip on Time Step
kube Dec 19, 2025
87b914c
Format
kube Dec 19, 2025
fc0baa7
Remove dependency of SimulationProvider on EditorContext and reorder …
kube Dec 23, 2025
0c3962d
Add useIsReadOnly hook
kube Dec 23, 2025
5f7fc61
Review: remove comment
kube Dec 23, 2025
96c2265
Remove globalMode check from TransitionProperties
kube Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions libs/@hashintel/petrinaut/src/components/glass-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { css, cx } from "@hashintel/ds-helpers/css";
import {
type CSSProperties,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";

import { RESIZE_HANDLE_OFFSET, RESIZE_HANDLE_SIZE } from "../constants/ui";

const panelContainerStyle = css({
position: "relative",
borderRadius: "[7px]",
backgroundColor: "[rgba(255, 255, 255, 0.7)]",
boxShadow: "[0 2px 11px rgba(0, 0, 0, 0.1)]",
border: "[1px solid rgba(255, 255, 255, 0.8)]",
});

const blurOverlayStyle = css({
position: "absolute",
inset: "[0]",
borderRadius: "[7px]",
pointerEvents: "none",
backdropFilter: "[blur(27px)]",
});

const contentContainerStyle = css({
position: "relative",
height: "[100%]",
width: "[100%]",
});

type ResizableEdge = "top" | "bottom" | "left" | "right";

interface ResizeConfig {
/** Which edge of the panel is resizable */
edge: ResizableEdge;
/** Callback when the size changes */
onResize: (newSize: number) => void;
/** Current size (width for left/right, height for top/bottom) */
size: number;
/** Minimum size constraint */
minSize?: number;
/** Maximum size constraint */
maxSize?: number;
}

interface GlassPanelProps {
/** Content to render inside the panel */
children: ReactNode;
/** Additional CSS class name for the panel container */
className?: string;
/** Inline styles for the panel container */
style?: CSSProperties;
/** Additional CSS class name for the content container */
contentClassName?: string;
/** Inline styles for the content container */
contentStyle?: CSSProperties;
/** Blur amount in pixels (default: 24) */
blur?: number;
/** Configuration for making the panel resizable */
resizable?: ResizeConfig;
}

const getResizeHandleStyle = (edge: ResizableEdge): CSSProperties => {
const base: CSSProperties = {
position: "absolute",
background: "transparent",
border: "none",
padding: 0,
zIndex: 1001,
};

switch (edge) {
case "top":
return {
...base,
top: RESIZE_HANDLE_OFFSET,
left: 0,
right: 0,
height: RESIZE_HANDLE_SIZE,
cursor: "ns-resize",
};
case "bottom":
return {
...base,
bottom: RESIZE_HANDLE_OFFSET,
left: 0,
right: 0,
height: RESIZE_HANDLE_SIZE,
cursor: "ns-resize",
};
case "left":
return {
...base,
top: 0,
left: RESIZE_HANDLE_OFFSET,
bottom: 0,
width: RESIZE_HANDLE_SIZE,
cursor: "ew-resize",
};
case "right":
return {
...base,
top: 0,
right: RESIZE_HANDLE_OFFSET,
bottom: 0,
width: RESIZE_HANDLE_SIZE,
cursor: "ew-resize",
};
}
};

const getCursorStyle = (edge: ResizableEdge): string => {
return edge === "top" || edge === "bottom" ? "ns-resize" : "ew-resize";
};

/**
* GlassPanel provides a frosted glass-like appearance with backdrop blur.
*
* Uses a separate overlay element for the backdrop-filter to avoid
* interfering with child components that use fixed/absolute positioning
* (e.g., Monaco Editor hover widgets).
*
* Optionally supports resizing from any edge with the `resizable` prop.
*/
export const GlassPanel: React.FC<GlassPanelProps> = ({
children,
className,
style,
contentClassName,
contentStyle,
blur = 24,
resizable,
}) => {
const [isResizing, setIsResizing] = useState(false);
const resizeStartPosRef = useRef(0);
const resizeStartSizeRef = useRef(0);

const handleResizeStart = useCallback(
(event: React.MouseEvent) => {
if (!resizable) {
return;
}

event.preventDefault();
setIsResizing(true);

const isVertical =
resizable.edge === "top" || resizable.edge === "bottom";
resizeStartPosRef.current = isVertical ? event.clientY : event.clientX;
resizeStartSizeRef.current = resizable.size;
},
[resizable],
);

const handleResizeMove = useCallback(
(event: MouseEvent) => {
if (!isResizing || !resizable) {
return;
}

const { edge, onResize, minSize = 100, maxSize = 800 } = resizable;
const isVertical = edge === "top" || edge === "bottom";
const currentPos = isVertical ? event.clientY : event.clientX;

// Calculate delta based on edge direction
// For top/left: dragging towards origin increases size
// For bottom/right: dragging away from origin increases size
let delta: number;
if (edge === "top" || edge === "left") {
delta = resizeStartPosRef.current - currentPos;
} else {
delta = currentPos - resizeStartPosRef.current;
}

const newSize = Math.max(
minSize,
Math.min(maxSize, resizeStartSizeRef.current + delta),
);

onResize(newSize);
},
[isResizing, resizable],
);

const handleResizeEnd = useCallback(() => {
setIsResizing(false);
}, []);

// Handle keyboard resize
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (!resizable) {
return;
}

const { edge, onResize, size, minSize = 100, maxSize = 800 } = resizable;
const step = 10;
let delta = 0;

if (edge === "top" || edge === "bottom") {
if (event.key === "ArrowUp") {
delta = edge === "top" ? step : -step;
} else if (event.key === "ArrowDown") {
delta = edge === "top" ? -step : step;
}
} else if (event.key === "ArrowLeft") {
delta = edge === "left" ? step : -step;
} else if (event.key === "ArrowRight") {
delta = edge === "left" ? -step : step;
}

if (delta !== 0) {
const newSize = Math.max(minSize, Math.min(maxSize, size + delta));
onResize(newSize);
}
},
[resizable],
);

// Global cursor and event listeners during resize
useEffect(() => {
if (!isResizing || !resizable) {
return;
}

document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = getCursorStyle(resizable.edge);
document.body.style.userSelect = "none";

return () => {
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, resizable, handleResizeMove, handleResizeEnd]);

return (
<div className={cx(panelContainerStyle, className)} style={style}>
{/* Resize handle */}
{resizable && (
<button
type="button"
aria-label={`Resize panel from ${resizable.edge}`}
onMouseDown={handleResizeStart}
onKeyDown={handleKeyDown}
style={getResizeHandleStyle(resizable.edge)}
/>
)}

{/* Blur overlay - separate from content to avoid affecting child positioning */}
<div
className={blurOverlayStyle}
style={blur !== 24 ? { backdropFilter: `blur(${blur}px)` } : undefined}
/>

{/* Content container */}
<div
className={cx(contentContainerStyle, contentClassName)}
style={contentStyle}
>
{children}
</div>
</div>
);
};
25 changes: 25 additions & 0 deletions libs/@hashintel/petrinaut/src/constants/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* UI-related constants for the Petrinaut editor.
*/

// Panel margin (spacing around panels)
export const PANEL_MARGIN = 10;

// Resize handle
export const RESIZE_HANDLE_SIZE = 20;
export const RESIZE_HANDLE_OFFSET = -Math.floor(RESIZE_HANDLE_SIZE / 2);

// Left Sidebar
export const DEFAULT_LEFT_SIDEBAR_WIDTH = 320;
export const MIN_LEFT_SIDEBAR_WIDTH = 280;
export const MAX_LEFT_SIDEBAR_WIDTH = 500;

// Properties Panel (right side)
export const DEFAULT_PROPERTIES_PANEL_WIDTH = 450;
export const MIN_PROPERTIES_PANEL_WIDTH = 250;
export const MAX_PROPERTIES_PANEL_WIDTH = 800;

// Bottom Panel
export const DEFAULT_BOTTOM_PANEL_HEIGHT = 180;
export const MIN_BOTTOM_PANEL_HEIGHT = 100;
export const MAX_BOTTOM_PANEL_HEIGHT = 600;
2 changes: 1 addition & 1 deletion libs/@hashintel/petrinaut/src/examples/broken-machines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export const productionMachines: { title: string; petriNetDefinition: SDCPN } =
lambdaType: "predicate",
lambdaCode: "export default Lambda(() => true)",
transitionKernelCode:
"/**\n* This function defines the kernel for the transition.\n* It receives tokens from input places,\n* and any global parameters defined,\n* and should return tokens for output places keyed by place name.\n*/\nexport default TransitionKernel((tokens) => {\n // tokens is an object which looks like:\n // { PlaceA: [{ x: 0, y: 0 }], PlaceB: [...] }\n // where 'x' and 'y' are examples of dimensions (properties)\n // of the token's type.\n\n // Return an object with output place names as keys\n return {\n MachinesBeingRepaired: [\n { machine_damage_ratio: tokens.BrokenMachines[0].machine_damage_ratio }\n ],\n };\n});",
"/**\n* This function defines the kernel for the transition.\n* It receives tokens from input places,\n* and any global parameters defined,\n* and should return tokens for output places keyed by place name.\n*/\nexport default TransitionKernel((tokens) => {\n // tokens is an object which looks like:\n // { PlaceA: [{ x: 0, y: 0 }], PlaceB: [...] }\n // where 'x' and 'y' are examples of dimensions (properties)\n // of the token's type.\n\n // Return an object with output place names as keys\n return {\n MachinesBeingRepaired: [\n { machine_damage_ratio: tokens.MachinesToRepair[0].machine_damage_ratio }\n ],\n };\n});",
x: 1635,
y: 480,
width: 160,
Expand Down
1 change: 0 additions & 1 deletion libs/@hashintel/petrinaut/src/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const FEATURE_FLAGS = {
RUNNING_MAN_ICON: true,
REORDER_TRANSITION_ARCS: false,
};
17 changes: 7 additions & 10 deletions libs/@hashintel/petrinaut/src/petrinaut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@ export type PetrinautProps = {
/**
* Create a new net and load it into the editor.
*/
createNewNet: (params: {
petriNetDefinition: SDCPN;
title: string;
}) => void;
createNewNet: (params: { petriNetDefinition: SDCPN; title: string }) => void;
/**
* Whether to hide controls relating to net loading, creation and title.
*/
Expand Down Expand Up @@ -105,14 +102,14 @@ export const Petrinaut = ({
}: PetrinautProps) => {
return (
<SDCPNProvider {...rest}>
<EditorProvider>
<CheckerProvider>
<SimulationProvider>
<CheckerProvider>
<SimulationProvider>
<EditorProvider>
<MonacoSetup />
<EditorView hideNetManagementControls={hideNetManagementControls} />
</SimulationProvider>
</CheckerProvider>
</EditorProvider>
</EditorProvider>
</SimulationProvider>
</CheckerProvider>
</SDCPNProvider>
);
};
Loading
Loading