From 9122d4c0c7e61506487b27626a8d298d668cef8d Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Fri, 11 Sep 2026 04:52:29 +0200 Subject: [PATCH 01/45] Add alternate code editor layouts and direct navigation --- .changeset/clear-code-editor-guide.md | 5 + .changeset/tidy-code-editor-layouts.md | 5 + libs/@hashintel/petrinaut-core/src/ai.ts | 3 + libs/@hashintel/petrinaut/docs/README.md | 1 + libs/@hashintel/petrinaut/docs/code-editor.md | 29 ++ .../petrinaut/docs/visual-settings.md | 4 + .../src/react/state/user-settings-context.ts | 13 + .../state/user-settings-provider.test.tsx | 34 ++ .../react/state/user-settings-provider.tsx | 13 + .../ui/monaco/code-editor-layout-settings.tsx | 158 ++++++++++ .../src/ui/monaco/code-workspace.stories.tsx | 81 +++++ .../src/ui/monaco/code-workspace.test.tsx | 217 +++++++++++++ .../src/ui/monaco/code-workspace.tsx | 238 ++++++++++++++ .../src/ui/monaco/code-workspace/entries.ts | 123 ++++++++ .../src/ui/monaco/code-workspace/surface.tsx | 292 ++++++++++++++++++ .../src/ui/views/Editor/editor-view.test.tsx | 1 + .../src/ui/views/Editor/editor-view.tsx | 69 ++++- .../user-settings/user-settings-dialog.tsx | 10 + .../LeftSideBar/subviews/entities-tree.tsx | 39 ++- .../subviews/code-field.tsx | 2 +- .../subviews/main.tsx | 12 +- .../place-properties/subviews/main.tsx | 21 ++ .../subviews/place-visualizer/subview.tsx | 14 +- .../transition-properties/main.tsx | 4 +- .../transition-firing-time/subview.tsx | 13 +- .../subviews/transition-results/subview.tsx | 22 +- .../create-experiment-drawer.test.tsx | 3 + .../petrinaut-docs-content.ts | 2 + 28 files changed, 1388 insertions(+), 40 deletions(-) create mode 100644 .changeset/clear-code-editor-guide.md create mode 100644 .changeset/tidy-code-editor-layouts.md create mode 100644 libs/@hashintel/petrinaut/docs/code-editor.md create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.tsx create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/entries.ts create mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/surface.tsx diff --git a/.changeset/clear-code-editor-guide.md b/.changeset/clear-code-editor-guide.md new file mode 100644 index 00000000000..420edfdbafc --- /dev/null +++ b/.changeset/clear-code-editor-guide.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut-core": patch +--- + +Make the code editor guide available to the in-app assistant. diff --git a/.changeset/tidy-code-editor-layouts.md b/.changeset/tidy-code-editor-layouts.md new file mode 100644 index 00000000000..3b758f30504 --- /dev/null +++ b/.changeset/tidy-code-editor-layouts.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Open model functions in full screen, the properties panel, or a bottom dock with remembered layouts and direct navigation. diff --git a/libs/@hashintel/petrinaut-core/src/ai.ts b/libs/@hashintel/petrinaut-core/src/ai.ts index 894cb7126d7..0a79ad27d0e 100644 --- a/libs/@hashintel/petrinaut-core/src/ai.ts +++ b/libs/@hashintel/petrinaut-core/src/ai.ts @@ -108,6 +108,7 @@ export const petrinautDocNames = [ "preview", "ai-assistant", "visual-settings", + "code-editor", "compilation-output", "examples", ] as const; @@ -135,6 +136,8 @@ export const petrinautDocSummaries: Record = { "Compact read-only PetrinautPreview for host-controlled embeds: shared SDCPN canvas, pan/zoom/fit/minimap, selection and responsive inspector, root/subnet navigation, URL-state ownership, omitted editing and management UI, and host-owned iframe security.", "ai-assistant": "In-app AI assistant: opening the panel, one text and Voice mode transcript/composer, waveform start, inline Voice state and provenance, typed handoff, consent/recovery, prompt chips, tool cards, read-only/simulate-mode rules, host configuration.", + "code-editor": + "Full-screen, properties-panel and bottom-dock code editors: feature flags, direct function navigation, remembered layout, automatic edits and read-only behavior.", "visual-settings": "Animations, keep-panels-mounted, minimap, snap-to-grid, compact vs classic nodes, partial selection, tree view, arc rendering style, compute backend, compilation output, parameter sweeps, in-browser optimization.", "compilation-output": diff --git a/libs/@hashintel/petrinaut/docs/README.md b/libs/@hashintel/petrinaut/docs/README.md index 7770302f632..72f01324067 100644 --- a/libs/@hashintel/petrinaut/docs/README.md +++ b/libs/@hashintel/petrinaut/docs/README.md @@ -39,6 +39,7 @@ Petrinaut has three global modes in the top bar, though **Actual** is only enabl - [Actual Mode](actual-mode.md) -- View a host-provided live Petri net execution, currently via Brunch. - [Embedded Preview](preview.md) -- Explore a compact, read-only Petri net embedded in a host application. - [AI Assistant](ai-assistant.md) -- Build, review, and revise nets with text or inline Voice mode. +- [Code Editor](code-editor.md) -- Open model functions in full screen, the properties panel, or a bottom dock. - [User Settings](visual-settings.md) -- Open preferences from any workspace tab and configure General, Viewport, Simulation, and Labs. - [Compilation Output](compilation-output.md) -- Inspect how your net's code compiled, and what stops it running on the GPU. - [Examples](examples.md) -- Walkthrough of the built-in example nets. diff --git a/libs/@hashintel/petrinaut/docs/code-editor.md b/libs/@hashintel/petrinaut/docs/code-editor.md new file mode 100644 index 00000000000..5899d941d24 --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/code-editor.md @@ -0,0 +1,29 @@ +# Code Editor + +Open a model’s code in a larger editor while keeping its place, transition, or differential equation in view. + +## Enable the layouts + +Open **Settings** using the canvas gear button. Under **Feature flags**, turn on **Code editor layouts**, or select a layout card: + +- **Full screen** replaces the canvas with code. The file’s left sidebar stays available. +- **Properties panel** fills the right-hand panel with code and a compact header. Drag its left edge to resize it. +- **Bottom dock** opens code below the canvas. Drag its top edge to resize it. + +Your chosen layout is remembered. Turn the feature off to return to the inline editors. + +## Open code directly + +Use the **Code** menu on the canvas to choose a transition’s predicate or stochastic rate (λ), its transition kernel, a differential equation, or an enabled place visualizer. + +You can also select an item in the canvas or left sidebar, then use the code section’s **More options** menu to open it in a specific layout. This works while the section is collapsed. Inside an expanded code section, **Open code editor** uses your preferred layout. + +The item’s menu in the left sidebar opens its code directly. For a place with dynamics, **Open equation code** beside its assigned differential equation opens that equation. + +## Navigate and edit + +The header shows the item’s kind and name. Open the name menu to jump to another function in the active net. Use the function buttons below the name to switch between a transition’s firing function and kernel. + +The **Code editor layout** button switches between layouts. Your code, undo history, cursor position, and diagnostics carry over. Enlarged editors show line numbers and wrap long lines; full screen and the bottom dock also show a code minimap. + +Changes apply automatically to the model. Read-only files and simulation modes keep code read-only. Use **Back to properties** or **Close code editor** to return to the canvas and property fields. Changing the file or active subnet closes the editor. diff --git a/libs/@hashintel/petrinaut/docs/visual-settings.md b/libs/@hashintel/petrinaut/docs/visual-settings.md index f5f858be507..722efa5ed41 100644 --- a/libs/@hashintel/petrinaut/docs/visual-settings.md +++ b/libs/@hashintel/petrinaut/docs/visual-settings.md @@ -190,3 +190,7 @@ Enable subnet definitions and component instances for hierarchical nets. This op ### Compilation output (experimental) Off by default. Adds a [Compilation](compilation-output.md) tab to the bottom panel. + +### Code editor layouts + +Enable **Code editor layouts** to open model functions in **Full screen**, **Properties panel**, or **Bottom dock**. Select a visual layout card to enable the feature and set the preferred layout. The choice is saved between sessions. See [Code Editor](code-editor.md) for navigation and editing. diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts b/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts index 3e5528a2467..159f33b9874 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts @@ -16,6 +16,8 @@ import type { TimelineChartType, } from "./editor-context"; +export type CodeEditorPlacement = "fullscreen" | "properties" | "bottom"; + export type ArcRendering = "smoothstep" | "bezier" | "custom"; export type SubViewSectionSettings = { @@ -55,6 +57,8 @@ export type UserSettings = { partialSelection: boolean; enableNetComponents: boolean; enableNotebookView: boolean; + enableCodeEditorWorkspace: boolean; + codeEditorPlacement: CodeEditorPlacement; /** * Persisted preference controlling whether the product walkthrough opens * automatically the next time the app initializes. The live open state is @@ -131,6 +135,9 @@ export type UserSettingsActions = { setPartialSelection: (value: boolean) => void; setEnableNetComponents: (value: boolean) => void; setEnableNotebookView: (value: boolean) => void; + + setEnableCodeEditorWorkspace: (value: boolean) => void; + setCodeEditorPlacement: (value: CodeEditorPlacement) => void; setShowWalkthroughOnInit: (value: boolean) => void; setWebGpuEnabled: (value: boolean) => void; setShowCompilationOutput: (value: boolean) => void; @@ -168,6 +175,9 @@ export const defaultUserSettings: UserSettings = { partialSelection: true, enableNetComponents: false, enableNotebookView: false, + + enableCodeEditorWorkspace: false, + codeEditorPlacement: "fullscreen", showWalkthroughOnInit: true, webGpuEnabled: false, showCompilationOutput: false, @@ -204,6 +214,9 @@ export const defaultUserSettingsContextValue: UserSettingsContextValue = { setPartialSelection: () => {}, setEnableNetComponents: () => {}, setEnableNotebookView: () => {}, + + setEnableCodeEditorWorkspace: () => {}, + setCodeEditorPlacement: () => {}, setShowWalkthroughOnInit: () => {}, setWebGpuEnabled: () => {}, setShowCompilationOutput: () => {}, diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx index 0f6ae45762d..b28894d4ab4 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx @@ -170,3 +170,37 @@ describe("UserSettingsProvider", () => { expect(screen.getByRole("button", { name: "editor: on" })).toBeTruthy(); }); }); + +const CodeLayoutProbe = () => { + const settings = use(UserSettingsContext); + return ( + + ); +}; + +it("remembers the code editor flag and placement across sessions", () => { + const view = render( + + + , + ); + fireEvent.click(screen.getByRole("button", { name: "inline" })); + expect(screen.getByRole("button", { name: "bottom" })).toBeTruthy(); + view.unmount(); + render( + + + , + ); + expect(screen.getByRole("button", { name: "bottom" })).toBeTruthy(); +}); diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx index 7a9eb8bbc59..d919dc5a35b 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx @@ -15,6 +15,7 @@ import type { } from "./editor-context"; import type { ArcRendering, + CodeEditorPlacement, SubViewSectionSettings, UserSettings, } from "./user-settings-context"; @@ -61,6 +62,11 @@ const loadSettings = (): UserSettings => { return { ...defaultUserSettings, ...parsed, + codeEditorPlacement: + parsed.codeEditorPlacement === "properties" || + parsed.codeEditorPlacement === "bottom" + ? parsed.codeEditorPlacement + : "fullscreen", // Someone who had selected the GPU globally keeps it available. webGpuEnabled: parsed.webGpuEnabled ?? computeBackend === "webgpu", }; @@ -130,6 +136,13 @@ const OwnedUserSettingsProvider: React.FC = ({ setState((prev) => ({ ...prev, partialSelection: value })), setEnableNetComponents: (value: boolean) => setState((prev) => ({ ...prev, enableNetComponents: value })), + setEnableCodeEditorWorkspace: (value: boolean) => + setState((previous) => ({ + ...previous, + enableCodeEditorWorkspace: value, + })), + setCodeEditorPlacement: (value: CodeEditorPlacement) => + setState((previous) => ({ ...previous, codeEditorPlacement: value })), setEnableNotebookView: (value: boolean) => setState((prev) => ({ ...prev, enableNotebookView: value })), setShowWalkthroughOnInit: (value: boolean) => diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx new file mode 100644 index 00000000000..c3a71cbe8a6 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx @@ -0,0 +1,158 @@ +import { use } from "react"; + +import { Toggle } from "@hashintel/ds-components"; +import { css } from "@hashintel/ds-helpers/css"; + +import { UserSettingsContext } from "../../react/state/user-settings-context"; + +const layouts = [ + { + value: "fullscreen", + label: "Full screen", + description: "Replace the canvas", + }, + { + value: "properties", + label: "Properties panel", + description: "Keep the canvas beside code", + }, + { + value: "bottom", + label: "Bottom dock", + description: "Keep the canvas above code", + }, +] as const; + +const headingStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + fontSize: "sm", + fontWeight: "medium", + color: "neutral.fg.heading", + gap: "2", +}); +const descriptionStyle = css({ + fontSize: "xs", + color: "neutral.fg.subtle", + marginTop: "1", + marginBottom: "3", + lineHeight: "[1.5]", +}); +const gridStyle = css({ + display: "grid", + gridTemplateColumns: "[repeat(3, minmax(0, 1fr))]", + gap: "2", + marginBottom: "3", +}); +const cardStyle = css({ + padding: "2", + borderWidth: "thin", + borderColor: "neutral.bd.subtle", + borderRadius: "lg", + textAlign: "left", + cursor: "pointer", + background: "neutral.s00", + fontSize: "xs", + color: "neutral.fg.heading", + _hover: { background: "bg.subtle" }, + _focusVisible: { outline: "[2px solid #2563eb]", outlineOffset: "[2px]" }, + "&[aria-pressed=true]": { borderColor: "[#3b82f6]", background: "[#eff6ff]" }, +}); +const previewStyle = css({ + height: "[60px]", + position: "relative", + overflow: "hidden", + borderRadius: "sm", + borderWidth: "thin", + borderColor: "neutral.bd.subtle", + background: "[#f5f6f8]", + marginBottom: "2", +}); +const barStyle = css({ + position: "absolute", + inset: "[0 0 auto]", + height: "[7px]", + background: "[#e2e5eb]", +}); +const sidebarStyle = css({ + position: "absolute", + top: "[7px]", + bottom: "[0]", + width: "[16%]", + borderRight: "[1px solid #e2e5eb]", + background: "neutral.s00", +}); +const codeStyle = css({ + position: "absolute", + padding: "[4px]", + background: "neutral.s00", + border: "[1px solid #c6d9fa]", + color: "[#4d7cc0]", + fontSize: "[8px]", + lineHeight: "[9px]", + fontFamily: "[monospace]", + whiteSpace: "pre", + overflow: "hidden", +}); +const labelStyle = css({ fontWeight: "medium", lineHeight: "[1.3]" }); + +export const CodeEditorLayoutSettings = () => { + const { + enableCodeEditorWorkspace, + setEnableCodeEditorWorkspace, + codeEditorPlacement, + setCodeEditorPlacement, + } = use(UserSettingsContext); + return ( + <> +
+ Code editor layouts + +
+

+ Open code directly from a field’s menu or the canvas Code menu. Choose + where it opens. +

+
+ {layouts.map((layout) => ( + + ))} +
+ + ); +}; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx new file mode 100644 index 00000000000..361c1f0af80 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx @@ -0,0 +1,81 @@ +import { use, useState } from "react"; +import { userEvent, within } from "storybook/test"; + +import { css } from "@hashintel/ds-helpers/css"; +import { productionMachines } from "@hashintel/petrinaut-core/examples"; + +import { UserSettingsContext } from "../../react/state/user-settings-context"; +import { UserSettingsProvider } from "../../react/state/user-settings-provider"; +import { PetrinautStoryProvider } from "../petrinaut-story-provider"; + +import type { CodeEditorPlacement } from "../../react/state/user-settings-context"; +import type { Meta, StoryObj } from "@storybook/react-vite"; + +const LayoutExample = ({ + placement, + readonly = false, +}: { + placement: CodeEditorPlacement; + readonly?: boolean; +}) => { + const settings = use(UserSettingsContext); + const [enabled, setEnabled] = useState(true); + const [layout, setLayout] = useState(placement); + return ( + +
+ +
+
+ ); +}; + +const meta = { + title: "Petrinaut / Code editor layouts", + component: LayoutExample, + parameters: { layout: "fullscreen" }, + decorators: [ + (Story) => ( + + + + ), + ], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click( + await canvas.findByRole( + "button", + { name: "Open code" }, + { timeout: 15000 }, + ), + ); + const page = within(canvasElement.ownerDocument.body); + await userEvent.click( + await page.findByRole("menuitem", { + name: "Production Success / Transition kernel", + }), + ); + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const FullScreen: Story = { args: { placement: "fullscreen" } }; +export const PropertiesPanel: Story = { args: { placement: "properties" } }; +export const BottomDock: Story = { args: { placement: "bottom" } }; +export const ReadOnly: Story = { + args: { placement: "properties", readonly: true }, +}; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx new file mode 100644 index 00000000000..070feff5347 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx @@ -0,0 +1,217 @@ +/** @vitest-environment jsdom */ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { use, useState } from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { DEFAULT_PETRINAUT_EXTENSIONS } from "@hashintel/petrinaut-core"; +import { productionMachines } from "@hashintel/petrinaut-core/examples"; + +import { ActiveNetContext } from "../../react/state/active-net-context"; +import { EditorContext } from "../../react/state/editor-context"; +import { SDCPNContext } from "../../react/state/sdcpn-context"; +import { + defaultUserSettingsContextValue, + UserSettingsContext, +} from "../../react/state/user-settings-context"; +import { CodeWorkspaceProvider, useCodeWorkspace } from "./code-workspace"; +import { getCodeEntries } from "./code-workspace/entries"; + +import type { CodeEditorPlacement } from "../../react/state/user-settings-context"; + +const mutations = vi.hoisted(() => ({ + updatePlace: vi.fn(), + updateTransition: vi.fn(), + updateDifferentialEquation: vi.fn(), +})); +vi.mock("../../react", () => ({ usePetrinautMutations: () => mutations })); +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +const definition = productionMachines.petriNetDefinition; +const net = { + ...definition, + componentInstances: definition.componentInstances ?? [], +}; +const selectItem = vi.fn(); + +const Probe = () => { + const workspace = useCodeWorkspace(); + const settings = use(UserSettingsContext); + return ( + <> + {workspace.activePath ?? "closed"} + {settings.codeEditorPlacement} + + + + {workspace.entries.map((entry) => ( + + ))} + + ); +}; + +const Harness = ({ + documentId = "one", + empty = false, + enabled = true, +}: { + documentId?: string; + empty?: boolean; + enabled?: boolean; +}) => { + const defaults = use(SDCPNContext); + const editorDefaults = use(EditorContext); + const [featureEnabled, setFeatureEnabled] = useState(enabled); + const [placement, setPlacement] = useState("fullscreen"); + const activeNet = empty + ? { ...net, transitions: [], differentialEquations: [], places: [] } + : net; + return ( + + {} }} + > + + + + + + + + + + ); +}; + +const firstCodeButton = () => + screen + .getAllByRole("button") + .find((button) => button.textContent.includes(" / "))!; + +describe("code workspace navigation", () => { + it("opens a function directly, selects its owner, and remembers its layout", () => { + render(); + fireEvent.click(firstCodeButton()); + expect(screen.getByTestId("active").textContent).not.toBe("closed"); + expect(selectItem).toHaveBeenCalledOnce(); + expect(screen.getByTestId("placement").textContent).toBe("bottom"); + const path = screen.getByTestId("active").textContent; + fireEvent.click(screen.getByText("Change layout")); + expect(screen.getByTestId("active").textContent).toBe(path); + expect(screen.getByTestId("placement").textContent).toBe("properties"); + fireEvent.click(screen.getByText("Close")); + expect(screen.getByTestId("active").textContent).toBe("closed"); + }); + + it("does not open while disabled and closes when the flag is disabled", () => { + const view = render(); + fireEvent.click(firstCodeButton()); + expect(screen.getByTestId("active").textContent).toBe("closed"); + view.unmount(); + render(); + fireEvent.click(firstCodeButton()); + fireEvent.click(screen.getByText("Disable")); + expect(screen.getByTestId("active").textContent).toBe("closed"); + }); + + it("clears the function when the document changes even with identical entity IDs", () => { + const view = render(); + fireEvent.click(firstCodeButton()); + view.rerender(); + expect(screen.getByTestId("active").textContent).toBe("closed"); + }); + + it("clears the function when its entity disappears", () => { + const view = render(); + fireEvent.click(firstCodeButton()); + view.rerender(); + expect(screen.getByTestId("active").textContent).toBe("closed"); + }); +}); + +describe("code entries", () => { + it("routes edits to the matching transition and equation", () => { + const entries = getCodeEntries( + net, + definition, + DEFAULT_PETRINAUT_EXTENSIONS, + mutations, + ); + const kernel = entries.find((entry) => entry.label === "Transition kernel"); + expect(kernel).toBeDefined(); + kernel?.update("updated kernel"); + expect(mutations.updateTransition).toHaveBeenCalledWith({ + transitionId: kernel?.selection.id, + update: { transitionKernelCode: "updated kernel" }, + }); + const equation = entries.find( + (entry) => entry.ownerKind === "Differential equation", + ); + expect(equation).toBeDefined(); + equation?.update("updated equation"); + expect(mutations.updateDifferentialEquation).toHaveBeenCalledWith({ + equationId: equation?.selection.id, + update: { code: "updated equation" }, + }); + expect(new Set(entries.map((entry) => entry.path)).size).toBe( + entries.length, + ); + }); + + it("excludes disabled transition logic and dynamics", () => { + const entries = getCodeEntries( + net, + definition, + { + ...DEFAULT_PETRINAUT_EXTENSIONS, + colors: false, + dynamics: false, + + stochasticity: false, + }, + mutations, + ); + expect(entries.some((entry) => entry.label === "Transition kernel")).toBe( + false, + ); + expect( + entries.some((entry) => entry.ownerKind === "Differential equation"), + ).toBe(false); + }); +}); diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.tsx new file mode 100644 index 00000000000..695ccacb0f8 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.tsx @@ -0,0 +1,238 @@ +import { createContext, use, useEffect, useRef, useState } from "react"; + +import { Button, Menu, type MenuItem } from "@hashintel/ds-components"; +import { css } from "@hashintel/ds-helpers/css"; + +import { usePetrinautMutations } from "../../react"; +import { ActiveNetContext } from "../../react/state/active-net-context"; +import { EditorContext } from "../../react/state/editor-context"; +import { SDCPNContext } from "../../react/state/sdcpn-context"; +import { UserSettingsContext } from "../../react/state/user-settings-context"; +import { usePetrinautPresentation } from "../views/shared/presentation-context"; +import { CodeEditor, type CodeEditorProps } from "./code-editor"; +import { getCodeEntries, type CodeEntry } from "./code-workspace/entries"; +import { CodeWorkspaceSurface } from "./code-workspace/surface"; + +import type { CodeEditorPlacement } from "../../react/state/user-settings-context"; +import type { editor } from "monaco-editor"; +import type { PropsWithChildren } from "react"; + +export const codeEditorPlacements: { + value: CodeEditorPlacement; + label: string; +}[] = [ + { value: "fullscreen", label: "Full screen" }, + { value: "properties", label: "Properties panel" }, + { value: "bottom", label: "Bottom dock" }, +]; + +type CodeWorkspace = { + enabled: boolean; + entries: CodeEntry[]; + activePath: string | null; + open: (path: string, placement?: CodeEditorPlacement) => void; + close: () => void; + retainModel: (model: editor.ITextModel | null) => void; +}; + +const CodeWorkspaceContext = createContext({ + enabled: false, + entries: [], + activePath: null, + open: () => {}, + close: () => {}, + retainModel: () => {}, +}); + +export const CodeWorkspaceProvider = ({ children }: PropsWithChildren) => { + const { activeNet, activeSubnetId } = use(ActiveNetContext); + const { petriNetDefinition, petriNetId, extensions } = use(SDCPNContext); + const scope = `${petriNetId ?? "unsaved"}/${activeSubnetId ?? "root"}`; + const [previousScope, setPreviousScope] = useState(scope); + const { selectItem } = use(EditorContext); + const { + enableCodeEditorWorkspace, + setCodeEditorPlacement, + updateSubViewSection, + } = use(UserSettingsContext); + const { showSourceCode } = usePetrinautPresentation(); + const mutations = usePetrinautMutations(); + const [activePath, setActivePath] = useState(null); + const models = useRef(new Set()); + const entries = getCodeEntries( + activeNet, + petriNetDefinition, + extensions, + mutations, + ); + const enabled = enableCodeEditorWorkspace && showSourceCode; + const activeEntry = enabled + ? entries.find((entry) => entry.path === activePath) + : undefined; + + if (scope !== previousScope) { + setPreviousScope(scope); + setActivePath(null); + } + + if (activePath !== null && !activeEntry) { + setActivePath(null); + } + + useEffect(() => { + const retainedModels = models.current; + return () => { + // Monaco's editor must detach before its retained model is disposed. + queueMicrotask(() => { + for (const model of retainedModels) { + if (!model.isDisposed() && !model.isAttachedToEditor()) + model.dispose(); + } + retainedModels.clear(); + }); + }; + }, []); + + return ( + { + const entry = entries.find((candidate) => candidate.path === path); + if (!enabled || !entry) return; + if (placement) setCodeEditorPlacement(placement); + selectItem(entry.selection); + setActivePath(path); + }, + close: () => { + if (activeEntry) { + if (activeEntry.selection.type === "transition") { + updateSubViewSection( + "transition-properties", + activeEntry.label === "Transition kernel" + ? "transition-results" + : "transition-firing-time", + { collapsed: false }, + ); + } else if (activeEntry.selection.type === "place") { + updateSubViewSection("place-properties", "place-visualizer", { + collapsed: false, + }); + } + selectItem(activeEntry.selection); + } + setActivePath(null); + }, + retainModel: (model) => { + if (model) models.current.add(model); + }, + }} + > + {children} + + ); +}; + +export const useCodeWorkspace = () => use(CodeWorkspaceContext); + +export const useCodeEditorMenuItems = (path: string): MenuItem[] => { + const { enabled, open } = useCodeWorkspace(); + return enabled + ? codeEditorPlacements.map((placement) => ({ + id: `code-${placement.value}`, + text: `Open in ${placement.label.toLowerCase()}`, + onClick: () => open(path, placement.value), + })) + : []; +}; + +const inlineStyle = css({ + display: "flex", + flexDirection: "column", + flex: "[1]", + minHeight: "[0]", + height: "full", +}); +const inlineHeaderStyle = css({ + display: "flex", + justifyContent: "flex-end", + paddingBottom: "1", +}); + +export const SourceCodeEditor = (props: CodeEditorProps) => { + const { enabled, activePath, open, retainModel } = useCodeWorkspace(); + const expanded = props.path !== undefined && props.path === activePath; + if (expanded) return null; + return ( +
+ {enabled && props.path && ( +
+ +
+ )} +
+ { + retainModel(instance.getModel()); + instance.onDidChangeModel(() => retainModel(instance.getModel())); + props.onMount?.(instance, monaco); + }} + /> +
+
+ ); +}; + +export const CodeWorkspacePanel = () => { + const workspace = useCodeWorkspace(); + const entry = workspace.entries.find( + (candidate) => candidate.path === workspace.activePath, + ); + return entry ? ( + + ) : null; +}; + +export const CodeWorkspaceMenu = () => { + const { enabled, entries, open } = useCodeWorkspace(); + if (!enabled || entries.length === 0) return null; + return ( + + Code + + } + items={entries.map((entry) => ({ + id: entry.path, + text: `${entry.owner} / ${entry.label}`, + onClick: () => open(entry.path), + }))} + /> + ); +}; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/entries.ts b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/entries.ts new file mode 100644 index 00000000000..7528ba95c71 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/entries.ts @@ -0,0 +1,123 @@ +import { + getEffectiveTransitionLambdaType, + getTransitionLogicAvailability, +} from "@hashintel/petrinaut-core"; + +import { getDocumentUri } from "../editor-paths"; + +import type { PetrinautMutations } from "../../../react"; +import type { ActiveNetDefinition } from "../../../react/state/active-net-context"; +import type { SDCPNContextValue } from "../../../react/state/sdcpn-context"; +import type { SelectionItem } from "@hashintel/petrinaut-core"; + +export type CodeEntry = { + path: string; + owner: string; + ownerKind: string; + label: string; + value: string; + selection: SelectionItem; + update: (value: string) => void; +}; + +export const getCodeEntries = ( + activeNet: ActiveNetDefinition, + sdcpn: SDCPNContextValue["petriNetDefinition"], + extensions: SDCPNContextValue["extensions"], + mutations: Pick< + PetrinautMutations, + "updatePlace" | "updateTransition" | "updateDifferentialEquation" + >, +): CodeEntry[] => [ + ...activeNet.transitions.flatMap((transition): CodeEntry[] => { + const availability = getTransitionLogicAvailability( + transition, + sdcpn, + extensions, + activeNet, + ); + const lambdaType = getEffectiveTransitionLambdaType( + transition, + availability, + ); + const shared = { + owner: transition.name, + ownerKind: "Transition", + selection: { type: "transition" as const, id: transition.id }, + }; + return [ + ...(availability.lambda + ? [ + { + ...shared, + path: getDocumentUri("transition-lambda", transition.id), + label: + lambdaType === "predicate" + ? "Predicate (λ)" + : "Stochastic rate (λ)", + value: + transition.lambdaType === lambdaType + ? transition.lambdaCode + : "", + update: (value: string) => + mutations.updateTransition({ + transitionId: transition.id, + update: { lambdaType, lambdaCode: value }, + }), + }, + ] + : []), + ...(availability.transitionKernel + ? [ + { + ...shared, + path: getDocumentUri("transition-kernel", transition.id), + label: "Transition kernel", + value: transition.transitionKernelCode, + update: (value: string) => + mutations.updateTransition({ + transitionId: transition.id, + update: { transitionKernelCode: value }, + }), + }, + ] + : []), + ]; + }), + ...(extensions.colors && extensions.dynamics + ? activeNet.differentialEquations.map( + (equation): CodeEntry => ({ + path: getDocumentUri("differential-equation", equation.id), + owner: equation.name, + ownerKind: "Differential equation", + label: "Differential equation", + value: equation.code, + selection: { type: "differentialEquation", id: equation.id }, + update: (value) => + mutations.updateDifferentialEquation({ + equationId: equation.id, + update: { code: value }, + }), + }), + ) + : []), + ...activeNet.places.flatMap((place): CodeEntry[] => + place.visualizerCode === undefined + ? [] + : [ + { + path: `inmemory://sdcpn/places/${place.id}/visualizer.tsx`, + owner: place.name, + ownerKind: "Place", + label: "Visualizer", + value: place.visualizerCode, + selection: { type: "place", id: place.id }, + update: (value) => + mutations.updatePlace({ + placeId: place.id, + update: { visualizerCode: value }, + }), + }, + ], + ), +]; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/surface.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/surface.tsx new file mode 100644 index 00000000000..65c199b83c5 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace/surface.tsx @@ -0,0 +1,292 @@ +import { use, useState } from "react"; + +import { Button, Icon, Menu } from "@hashintel/ds-components"; +import { css } from "@hashintel/ds-helpers/css"; + +import { EditorContext } from "../../../react/state/editor-context"; +import { useIsReadOnly } from "../../../react/state/use-is-read-only"; +import { UserSettingsContext } from "../../../react/state/user-settings-context"; +import { GlassPanel } from "../../components/glass-panel"; +import { UI_MESSAGES } from "../../constants/ui-messages"; +import { CodeEditor } from "../code-editor"; + +import type { CodeEditorPlacement } from "../../../react/state/user-settings-context"; +import type { CodeEntry } from "./entries"; +import type { editor } from "monaco-editor"; + +const surfaceStyle = css({ + position: "absolute", + top: "[0]", + right: "[0]", + bottom: "[0]", + zIndex: "[calc(var(--z-index-sticky) - 1)]", + background: "neutral.s00", + borderLeftWidth: "thin", + borderTopWidth: "thin", + borderColor: "neutral.bd.subtle", +}); +const contentStyle = css({ + display: "flex", + flexDirection: "column", + minHeight: "[0]", + height: "full", + overflow: "hidden", +}); +const headerStyle = css({ + display: "flex", + alignItems: "center", + gap: "1", + padding: "2", + borderBottomWidth: "thin", + borderColor: "neutral.bd.subtle", + flexShrink: 0, +}); +const ownerStyle = css({ minWidth: "[0]", flex: "[1]", overflow: "hidden" }); +const ownerKindStyle = css({ + fontSize: "[10px]", + fontWeight: "medium", + color: "neutral.fg.subtle", + paddingLeft: "2", +}); +const ownerButtonStyle = css({ + "&&": { maxWidth: "full", justifyContent: "flex-start" }, + "& span": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, +}); +const tabsStyle = css({ + display: "flex", + alignItems: "center", + gap: "1", + paddingX: "3", + paddingY: "2", + borderBottomWidth: "thin", + borderColor: "neutral.bd.subtle", + overflowX: "auto", + flexShrink: 0, +}); +const editorStyle = css({ + flex: "[1]", + minHeight: "[0]", + display: "flex", + flexDirection: "column", + "& > div": { borderRadius: "[0]", border: "[0]" }, +}); +const footerStyle = css({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: "2", + paddingX: "3", + paddingY: "2", + color: "neutral.fg.subtle", + fontSize: "xs", + borderTopWidth: "thin", + borderColor: "neutral.bd.subtle", + flexShrink: 0, +}); +const statusStyle = css({ + display: "flex", + alignItems: "center", + gap: "1", + minWidth: "[0]", +}); + +export const CodeWorkspaceSurface = ({ + entry, + entries, + onOpen, + onClose, + retainModel, + placements, +}: { + entry: CodeEntry; + entries: CodeEntry[]; + onOpen: (path: string, placement?: CodeEditorPlacement) => void; + onClose: () => void; + retainModel: (model: editor.ITextModel | null) => void; + placements: { value: CodeEditorPlacement; label: string }[]; +}) => { + const { codeEditorPlacement, setCodeEditorPlacement } = + use(UserSettingsContext); + const { + isLeftSidebarOpen, + leftSidebarWidth, + propertiesPanelWidth, + setPropertiesPanelWidth, + } = use(EditorContext); + const isReadOnly = useIsReadOnly(); + const [dockHeight, setDockHeight] = useState(360); + const [position, setPosition] = useState({ lineNumber: 1, column: 1 }); + const siblings = entries.filter( + (candidate) => candidate.selection.id === entry.selection.id, + ); + const placement = placements.find( + (candidate) => candidate.value === codeEditorPlacement, + ) ?? { value: "fullscreen", label: "Full screen" }; + + const sideWidth = Math.max(360, propertiesPanelWidth); + const left = isLeftSidebarOpen ? leftSidebarWidth : 0; + const panel = placement.value === "properties"; + const dock = placement.value === "bottom"; + + return ( + +
+
+ + } + items={entries.map((candidate) => ({ + id: candidate.path, + text: `${candidate.owner} / ${candidate.label}`, + onClick: () => onOpen(candidate.path), + }))} + /> + + + } + items={placements.map((candidate) => ({ + id: candidate.value, + text: candidate.label, + onClick: () => setCodeEditorPlacement(candidate.value), + }))} + /> +
+ +
+ { + if (value !== undefined && !isReadOnly) entry.update(value); + }} + options={{ + readOnly: isReadOnly, + lineNumbers: "on", + wordWrap: "on", + minimap: { enabled: !panel }, + automaticLayout: true, + fontSize: 13, + padding: { top: 16, bottom: 16 }, + ariaLabel: `${entry.owner}: ${entry.label}`, + }} + onMount={(mountedEditor) => { + setPosition( + mountedEditor.getPosition() ?? { lineNumber: 1, column: 1 }, + ); + retainModel(mountedEditor.getModel()); + mountedEditor.onDidChangeModel(() => { + retainModel(mountedEditor.getModel()); + setPosition( + mountedEditor.getPosition() ?? { lineNumber: 1, column: 1 }, + ); + }); + mountedEditor.onDidChangeCursorPosition((event) => + setPosition(event.position), + ); + mountedEditor.focus(); + }} + /> +
+
+ + + {isReadOnly ? "Read only" : "Changes apply automatically"} + + + Ln {position.lineNumber}, Col {position.column} + +
+
+
+ ); +}; diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.test.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.test.tsx index 8dfe8fd3ce5..b0db77e41b0 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.test.tsx @@ -20,6 +20,7 @@ const lifecycle = vi.hoisted(() => ({ vi.mock("../../../react", () => ({ usePetrinautCommands: () => ({ applyAutoLayout: vi.fn() }), + usePetrinautMutations: () => ({}), })); vi.mock("../../../react/state/use-selection-cleanup", () => ({ useSelectionCleanup: () => {}, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx index d32b82ce049..f3aa4a6a260 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx @@ -47,6 +47,12 @@ import { ExperimentalIconProvider } from "../../experimental-icons"; import { exportSDCPN } from "../../file-io/export-sdcpn"; import { exportTikZ } from "../../file-io/export-tikz"; import { importSDCPN } from "../../file-io/import-sdcpn"; +import { + CodeWorkspaceMenu, + CodeWorkspacePanel, + CodeWorkspaceProvider, + useCodeWorkspace, +} from "../../monaco/code-workspace"; import { NotebookView } from "../Notebook/notebook-view"; import { SDCPNView } from "../SDCPN/sdcpn-view"; import { AiCtaModal } from "./components/ai-cta-modal"; @@ -140,7 +146,7 @@ const isEmptySDCPN = (sdcpn: SDCPN) => * EditorView is responsible for the overall editor UI layout and controls. * It relies on sdcpn-store and editor-store for state, and uses SDCPNView for visualization. */ -export const EditorView = ({ +const EditorViewContent = ({ aiAssistant, hideNetManagementControls, slots, @@ -156,6 +162,10 @@ export const EditorView = ({ titleEditable: boolean; viewportActions?: ViewportAction[]; }) => { + const { activePath } = useCodeWorkspace(); + const { codeEditorPlacement } = use(UserSettingsContext); + const codeReplacesCanvas = + activePath !== null && codeEditorPlacement === "fullscreen"; const showNetManagementMenuItems = hideNetManagementControls === undefined; const navigation = usePetrinautNavigation(); // Auto-layout moves nodes, which a read-only net rejects, so the menu would @@ -187,6 +197,8 @@ export const EditorView = ({ setAiAssistantCollapsed, isBottomPanelOpen, bottomPanelHeight, + isLeftSidebarOpen, + leftSidebarWidth, } = use(EditorContext); const actualMode = use(ActualModeContext); @@ -590,10 +602,14 @@ export const EditorView = ({ {/* Properties Panel - Right Side */} - + {!activePath || codeEditorPlacement === "bottom" ? ( + + ) : null} {/* SDCPN Visualization */} - + {!codeReplacesCanvas && ( + + )} {showEmptyAiHero && ( )} + + {!codeReplacesCanvas && ( +
+ +
+ )} + {/* Bottom Panel */} - + {!activePath && } )} - {(effectiveMode === "edit" || effectiveMode === "actual") && ( - - )} + {!activePath && + (effectiveMode === "edit" || effectiveMode === "actual") && ( + + )} {aiAssistant && ( ); }; + +export const EditorView = ( + props: React.ComponentProps, +) => ( + + + +); diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view/user-settings/user-settings-dialog.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view/user-settings/user-settings-dialog.tsx index 2bcbcea49e1..887bc3a9406 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view/user-settings/user-settings-dialog.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view/user-settings/user-settings-dialog.tsx @@ -16,6 +16,7 @@ import { isConnectedOptimization } from "@hashintel/petrinaut-core/optimization" import { PetrinautOptimizationContext } from "../../../../../react/optimization-context"; import { SDCPNContext } from "../../../../../react/state/sdcpn-context"; import { UserSettingsContext } from "../../../../../react/state/user-settings-context"; +import { CodeEditorLayoutSettings } from "../../../../monaco/code-editor-layout-settings"; import { focusLands } from "../../../../worksheet/focus-flow"; import { FocusRoot, FocusStack } from "../../../../worksheet/focus-stack"; import { useFocusMember } from "../../../../worksheet/use-focus-member"; @@ -622,6 +623,15 @@ export const UserSettingsDialog = ({ /> )} + + + {() => } + + = ({ item }) => { const { globalMode } = use(EditorContext); const isReadOnly = useIsReadOnly(); + const { enabled, entries, open } = useCodeWorkspace(); + const codeItems = enabled + ? entries + .filter((entry) => entry.selection.id === item.id) + .map((entry) => ({ + id: entry.path, + text: `Open ${entry.label.toLowerCase()}`, + onClick: () => open(entry.path), + })) + : []; const type = item.selectionItem?.type; if (!type) { return null; } - // Nodes (places/transitions) don't have a row menu - if (type === "place" || type === "transition") { - return null; - } - // Parameters hide their menu in simulation mode if (type === "parameter" && globalMode === "simulate") { return null; @@ -76,21 +82,26 @@ const EntityRowMenu: React.FC<{ item: EntityTreeItem }> = ({ item }) => { }; const deleteAction = deleteActions[type]; - if (!deleteAction) { + if (!deleteAction && codeItems.length === 0) { return null; } return ( ); diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/code-field.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/code-field.tsx index 6db3c4d6544..c23c4c8f898 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/code-field.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/code-field.tsx @@ -3,7 +3,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { useIsReadOnly } from "../../../../../../../react/state/use-is-read-only"; import { UI_MESSAGES } from "../../../../../../constants/ui-messages"; -import { CodeEditor } from "../../../../../../monaco/code-editor"; +import { SourceCodeEditor as CodeEditor } from "../../../../../../monaco/code-workspace"; import { getDocumentUri } from "../../../../../../monaco/editor-paths"; import { useDiffEqPropertiesContext } from "../context"; diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx index c73cb049839..114f0ba04df 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx @@ -21,6 +21,8 @@ import { DraftFieldInput } from "../../../../../../components/draft-field-input" import { PropertyValue } from "../../../../../../components/property-value"; import { DifferentialEquationIcon } from "../../../../../../constants/entity-icons"; import { UI_MESSAGES } from "../../../../../../constants/ui-messages"; +import { useCodeEditorMenuItems } from "../../../../../../monaco/code-workspace"; +import { getDocumentUri } from "../../../../../../monaco/editor-paths"; import { usePetrinautPresentation } from "../../../../../shared/presentation-context"; import { useDiffEqPropertiesContext } from "../context"; @@ -251,7 +253,11 @@ const DiffEqCodeAction: React.FC = () => { useDiffEqPropertiesContext(); const isReadOnly = useIsReadOnly(); - if (isReadOnly) { + const codeEditorItems = useCodeEditorMenuItems( + getDocumentUri("differential-equation", differentialEquation.id), + ); + + if (isReadOnly && codeEditorItems.length === 0) { return null; } @@ -267,9 +273,11 @@ const DiffEqCodeAction: React.FC = () => { /> } items={[ + ...codeEditorItems, { id: "load-default", text: "Load default template", + disabled: isReadOnly, onClick: () => { const equationType = types.find( (tp) => tp.id === differentialEquation.colorId, @@ -315,6 +323,6 @@ export const diffEqMainContentSubView: SubView = { main: true, component: DiffEqMainContent, renderHeaderAction: () => , - headerActionMutates: true, + headerActionMutates: false, alwaysShowHeaderAction: true, }; diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/main.tsx index 04be855130e..a17497c95ad 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/main.tsx @@ -23,6 +23,8 @@ import { Section, SectionList } from "../../../../../../components/section"; import { PlaceIcon } from "../../../../../../constants/entity-icons"; import { UI_MESSAGES } from "../../../../../../constants/ui-messages"; import { useDraftField } from "../../../../../../hooks/use-draft-field"; +import { useCodeWorkspace } from "../../../../../../monaco/code-workspace"; +import { getDocumentUri } from "../../../../../../monaco/editor-paths"; import { usePlacePropertiesContext } from "../context"; import type { SubView } from "../../../../../../components/sub-view/types"; @@ -59,6 +61,7 @@ const arcStyle = css({ * Rendered as a headerless SubView at the top of the proportional layout. */ const PlaceMainContent: React.FC = () => { + const codeWorkspace = useCodeWorkspace(); const { place, types, isReadOnly, updatePlace } = usePlacePropertiesContext(); const { selectItem } = use(EditorContext); @@ -364,6 +367,24 @@ const PlaceMainContent: React.FC = () => { > Jump to Differential Equation + {codeWorkspace.enabled && ( + + )} )} diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx index f5006f17e66..2556f524119 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/place-visualizer/subview.tsx @@ -17,7 +17,10 @@ import { import { ExecutionFrameSourceContext } from "../../../../../../../../react/execution-frame/context"; import { EditorContext } from "../../../../../../../../react/state/editor-context"; import { UI_MESSAGES } from "../../../../../../../constants/ui-messages"; -import { CodeEditor } from "../../../../../../../monaco/code-editor"; +import { + useCodeEditorMenuItems, + SourceCodeEditor as CodeEditor, +} from "../../../../../../../monaco/code-workspace"; import { PlaceStateVisualization } from "../../../../../../shared/place-state-visualization"; import { usePlacePropertiesContext } from "../../context"; @@ -88,7 +91,7 @@ const VisualizerPreview: React.FC = () => { }; const PlaceVisualizerContent: React.FC = () => { - const { place, updatePlace } = usePlacePropertiesContext(); + const { place, updatePlace, isReadOnly } = usePlacePropertiesContext(); const { totalFrames } = use(ExecutionFrameSourceContext); const [viewMode, setViewMode] = useState("code"); @@ -131,6 +134,7 @@ const PlaceVisualizerContent: React.FC = () => { path={`inmemory://sdcpn/places/${place.id}/visualizer.tsx`} language="typescript" height="100%" + options={{ readOnly: isReadOnly }} value={place.visualizerCode} onChange={(value) => { updatePlace({ @@ -155,6 +159,10 @@ const VisualizerHeaderAction: React.FC = () => { const { place, types, isReadOnly, updatePlace } = usePlacePropertiesContext(); const { globalMode } = use(EditorContext); + const codeEditorItems = useCodeEditorMenuItems( + `inmemory://sdcpn/places/${place.id}/visualizer.tsx`, + ); + const [savedVisualizerCodeState, setSavedVisualizerCodeState] = useState<{ placeId: string; code: string; @@ -215,9 +223,11 @@ const VisualizerHeaderAction: React.FC = () => { /> } items={[ + ...codeEditorItems, { id: "load-default", text: "Load default template", + disabled: isReadOnly, onClick: () => { const currentPlaceType = place.colorId ? types.find((type) => type.id === place.colorId) diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx index 7414f13d82b..28fc3ee5ead 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx @@ -27,7 +27,7 @@ const containerStyle = css({ const transitionFiringTimeSubView = createDeferredSubView({ id: "transition-firing-time", - headerActionMutates: true, + headerActionMutates: false, title: "Firing Time", defaultCollapsed: true, tooltip: @@ -45,7 +45,7 @@ const transitionFiringTimeSubView = createDeferredSubView({ const transitionResultsSubView = createDeferredSubView({ id: "transition-results", - headerActionMutates: true, + headerActionMutates: false, title: "Transition Results", defaultCollapsed: true, tooltip: diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx index 54e2508fa58..d1e97dfe54c 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx @@ -15,7 +15,10 @@ import { import { EditorContext } from "../../../../../../../../react/state/editor-context"; import { UI_MESSAGES } from "../../../../../../../constants/ui-messages"; -import { CodeEditor } from "../../../../../../../monaco/code-editor"; +import { + useCodeEditorMenuItems, + SourceCodeEditor as CodeEditor, +} from "../../../../../../../monaco/code-workspace"; import { getDocumentUri } from "../../../../../../../monaco/editor-paths"; import { useTransitionPropertiesContext } from "../../context"; @@ -56,10 +59,14 @@ const aiMenuItemStyle = css({ }); const FiringTimeHeaderAction: React.FC = () => { - const { logicAvailability, transition, updateTransition } = + const { logicAvailability, transition, updateTransition, isReadOnly } = useTransitionPropertiesContext(); const { globalMode } = use(EditorContext); + const codeEditorItems = useCodeEditorMenuItems( + getDocumentUri("transition-lambda", transition.id), + ); + if (globalMode !== "edit" || !logicAvailability.lambda) { return null; } @@ -81,9 +88,11 @@ const FiringTimeHeaderAction: React.FC = () => { /> } items={[ + ...codeEditorItems, { id: "load-default", text: "Load default template", + disabled: isReadOnly, onClick: () => { updateTransition({ transitionId: transition.id, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-results/subview.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-results/subview.tsx index f7bbaf2f7f1..ece8380be5a 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-results/subview.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-results/subview.tsx @@ -9,7 +9,10 @@ import { import { EditorContext } from "../../../../../../../../react/state/editor-context"; import { UI_MESSAGES } from "../../../../../../../constants/ui-messages"; -import { CodeEditor } from "../../../../../../../monaco/code-editor"; +import { + useCodeEditorMenuItems, + SourceCodeEditor as CodeEditor, +} from "../../../../../../../monaco/code-workspace"; import { getDocumentUri } from "../../../../../../../monaco/editor-paths"; import { useTransitionPropertiesContext } from "../../context"; @@ -78,10 +81,21 @@ const createTransitionArcPlaceResolver = ( }; const ResultsHeaderAction: React.FC = () => { - const { logicAvailability, transition, sdcpn, net, types, updateTransition } = - useTransitionPropertiesContext(); + const { + logicAvailability, + transition, + sdcpn, + net, + types, + updateTransition, + isReadOnly, + } = useTransitionPropertiesContext(); const { globalMode } = use(EditorContext); + const codeEditorItems = useCodeEditorMenuItems( + getDocumentUri("transition-kernel", transition.id), + ); + if (globalMode !== "edit" || !logicAvailability.transitionKernel) { return null; } @@ -98,9 +112,11 @@ const ResultsHeaderAction: React.FC = () => { /> } items={[ + ...codeEditorItems, { id: "load-default", text: "Load default template", + disabled: isReadOnly, onClick: () => { const resolveArcPlace = createTransitionArcPlaceResolver( sdcpn, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx index 1e49e49f500..10d6c13e9c1 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/create-experiment-drawer.test.tsx @@ -287,6 +287,9 @@ const TestProviders = ({ setPartialSelection: () => {}, setEnableNetComponents: () => {}, setEnableNotebookView: () => {}, + + setEnableCodeEditorWorkspace: () => {}, + setCodeEditorPlacement: () => {}, setShowWalkthroughOnInit: () => {}, setWebGpuEnabled: () => {}, setShowCompilationOutput: () => {}, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/petrinaut-docs-content.ts b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/petrinaut-docs-content.ts index d47fed371a8..378529c02c8 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/petrinaut-docs-content.ts +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/petrinaut-docs-content.ts @@ -6,6 +6,7 @@ import { import actualMode from "../../../../../../docs/actual-mode.md?raw"; import adHocScenarios from "../../../../../../docs/ad-hoc-scenarios.md?raw"; import aiAssistant from "../../../../../../docs/ai-assistant.md?raw"; +import codeEditor from "../../../../../../docs/code-editor.md?raw"; import compilationOutput from "../../../../../../docs/compilation-output.md?raw"; import drawingANet from "../../../../../../docs/drawing-a-net.md?raw"; import examples from "../../../../../../docs/examples.md?raw"; @@ -41,6 +42,7 @@ const rawDocsByName: Record = { preview, "ai-assistant": aiAssistant, "visual-settings": visualSettings, + "code-editor": codeEditor, "compilation-output": compilationOutput, examples, }; From f988705b6be9c5d37b1fe058dd2d56784c9227e4 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 15 Sep 2026 05:27:25 +0200 Subject: [PATCH 02/45] Keep code editing in the Properties Panel and full screen --- .changeset/tidy-code-editor-layouts.md | 2 +- libs/@hashintel/petrinaut-core/src/ai.ts | 2 +- libs/@hashintel/petrinaut/docs/README.md | 2 +- libs/@hashintel/petrinaut/docs/code-editor.md | 26 +-- .../petrinaut/docs/visual-settings.md | 4 - .../src/react/state/user-settings-context.ts | 13 -- .../state/user-settings-provider.test.tsx | 34 ---- .../react/state/user-settings-provider.tsx | 13 -- .../ui/monaco/code-editor-layout-settings.tsx | 158 ------------------ .../src/ui/monaco/code-workspace.stories.tsx | 41 ++--- .../src/ui/monaco/code-workspace.test.tsx | 139 +++++++++++---- .../src/ui/monaco/code-workspace.tsx | 105 ++++++------ .../src/ui/monaco/code-workspace/surface.tsx | 71 +------- .../src/ui/views/Editor/editor-view.tsx | 32 +--- .../user-settings/user-settings-dialog.tsx | 10 -- .../create-experiment-drawer.test.tsx | 3 - 16 files changed, 200 insertions(+), 455 deletions(-) delete mode 100644 libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx diff --git a/.changeset/tidy-code-editor-layouts.md b/.changeset/tidy-code-editor-layouts.md index 3b758f30504..45f669eaf7f 100644 --- a/.changeset/tidy-code-editor-layouts.md +++ b/.changeset/tidy-code-editor-layouts.md @@ -2,4 +2,4 @@ "@hashintel/petrinaut": patch --- -Open model functions in full screen, the properties panel, or a bottom dock with remembered layouts and direct navigation. +Expand model functions to full screen and return to their code fields in the Properties Panel. diff --git a/libs/@hashintel/petrinaut-core/src/ai.ts b/libs/@hashintel/petrinaut-core/src/ai.ts index 0a79ad27d0e..0f18105957d 100644 --- a/libs/@hashintel/petrinaut-core/src/ai.ts +++ b/libs/@hashintel/petrinaut-core/src/ai.ts @@ -137,7 +137,7 @@ export const petrinautDocSummaries: Record = { "ai-assistant": "In-app AI assistant: opening the panel, one text and Voice mode transcript/composer, waveform start, inline Voice state and provenance, typed handoff, consent/recovery, prompt chips, tool cards, read-only/simulate-mode rules, host configuration.", "code-editor": - "Full-screen, properties-panel and bottom-dock code editors: feature flags, direct function navigation, remembered layout, automatic edits and read-only behavior.", + "Code editing in the Properties Panel and full screen: direct function navigation, returning to property controls, automatic edits and read-only behavior.", "visual-settings": "Animations, keep-panels-mounted, minimap, snap-to-grid, compact vs classic nodes, partial selection, tree view, arc rendering style, compute backend, compilation output, parameter sweeps, in-browser optimization.", "compilation-output": diff --git a/libs/@hashintel/petrinaut/docs/README.md b/libs/@hashintel/petrinaut/docs/README.md index 72f01324067..62f80cdcf23 100644 --- a/libs/@hashintel/petrinaut/docs/README.md +++ b/libs/@hashintel/petrinaut/docs/README.md @@ -39,7 +39,7 @@ Petrinaut has three global modes in the top bar, though **Actual** is only enabl - [Actual Mode](actual-mode.md) -- View a host-provided live Petri net execution, currently via Brunch. - [Embedded Preview](preview.md) -- Explore a compact, read-only Petri net embedded in a host application. - [AI Assistant](ai-assistant.md) -- Build, review, and revise nets with text or inline Voice mode. -- [Code Editor](code-editor.md) -- Open model functions in full screen, the properties panel, or a bottom dock. +- [Code Editor](code-editor.md) -- Edit model functions in the Properties Panel or full screen. - [User Settings](visual-settings.md) -- Open preferences from any workspace tab and configure General, Viewport, Simulation, and Labs. - [Compilation Output](compilation-output.md) -- Inspect how your net's code compiled, and what stops it running on the GPU. - [Examples](examples.md) -- Walkthrough of the built-in example nets. diff --git a/libs/@hashintel/petrinaut/docs/code-editor.md b/libs/@hashintel/petrinaut/docs/code-editor.md index 5899d941d24..4420042397b 100644 --- a/libs/@hashintel/petrinaut/docs/code-editor.md +++ b/libs/@hashintel/petrinaut/docs/code-editor.md @@ -1,29 +1,19 @@ # Code Editor -Open a model’s code in a larger editor while keeping its place, transition, or differential equation in view. +Edit a model’s functions in the Properties Panel or expand a code field to full screen. -## Enable the layouts +## Open code -Open **Settings** using the canvas gear button. Under **Feature flags**, turn on **Code editor layouts**, or select a layout card: +Select a place, transition, or differential equation in the canvas or left sidebar. Expand its code section in the Properties Panel, then use **Full screen** above the code to enlarge it. -- **Full screen** replaces the canvas with code. The file’s left sidebar stays available. -- **Properties panel** fills the right-hand panel with code and a compact header. Drag its left edge to resize it. -- **Bottom dock** opens code below the canvas. Drag its top edge to resize it. - -Your chosen layout is remembered. Turn the feature off to return to the inline editors. - -## Open code directly - -Use the **Code** menu on the canvas to choose a transition’s predicate or stochastic rate (λ), its transition kernel, a differential equation, or an enabled place visualizer. - -You can also select an item in the canvas or left sidebar, then use the code section’s **More options** menu to open it in a specific layout. This works while the section is collapsed. Inside an expanded code section, **Open code editor** uses your preferred layout. +The code section’s **More options** menu also offers **Open in full screen** and **Open in properties panel**. The Properties Panel keeps the selected item’s fields and controls alongside its code. Drag the panel’s left edge or the divider between sections to resize it. The item’s menu in the left sidebar opens its code directly. For a place with dynamics, **Open equation code** beside its assigned differential equation opens that equation. -## Navigate and edit +## Full-screen editing -The header shows the item’s kind and name. Open the name menu to jump to another function in the active net. Use the function buttons below the name to switch between a transition’s firing function and kernel. +Full screen replaces the canvas with code and leaves the left sidebar available. The header shows the item’s kind and name. Open the name menu to jump to another function in the active net. Use the function buttons below the name to switch between a transition’s firing function and kernel. -The **Code editor layout** button switches between layouts. Your code, undo history, cursor position, and diagnostics carry over. Enlarged editors show line numbers and wrap long lines; full screen and the bottom dock also show a code minimap. +Use **Back to properties** or **Close code editor** to return to the same code section in the Properties Panel. Code, undo history, cursor position, and diagnostics carry over. Full-screen code shows line numbers, wrapped lines, and a minimap. -Changes apply automatically to the model. Read-only files and simulation modes keep code read-only. Use **Back to properties** or **Close code editor** to return to the canvas and property fields. Changing the file or active subnet closes the editor. +Changes apply automatically to the model. Read-only files and simulation modes keep code read-only. Changing the file or active subnet closes the editor. diff --git a/libs/@hashintel/petrinaut/docs/visual-settings.md b/libs/@hashintel/petrinaut/docs/visual-settings.md index 722efa5ed41..f5f858be507 100644 --- a/libs/@hashintel/petrinaut/docs/visual-settings.md +++ b/libs/@hashintel/petrinaut/docs/visual-settings.md @@ -190,7 +190,3 @@ Enable subnet definitions and component instances for hierarchical nets. This op ### Compilation output (experimental) Off by default. Adds a [Compilation](compilation-output.md) tab to the bottom panel. - -### Code editor layouts - -Enable **Code editor layouts** to open model functions in **Full screen**, **Properties panel**, or **Bottom dock**. Select a visual layout card to enable the feature and set the preferred layout. The choice is saved between sessions. See [Code Editor](code-editor.md) for navigation and editing. diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts b/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts index 159f33b9874..3e5528a2467 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts @@ -16,8 +16,6 @@ import type { TimelineChartType, } from "./editor-context"; -export type CodeEditorPlacement = "fullscreen" | "properties" | "bottom"; - export type ArcRendering = "smoothstep" | "bezier" | "custom"; export type SubViewSectionSettings = { @@ -57,8 +55,6 @@ export type UserSettings = { partialSelection: boolean; enableNetComponents: boolean; enableNotebookView: boolean; - enableCodeEditorWorkspace: boolean; - codeEditorPlacement: CodeEditorPlacement; /** * Persisted preference controlling whether the product walkthrough opens * automatically the next time the app initializes. The live open state is @@ -135,9 +131,6 @@ export type UserSettingsActions = { setPartialSelection: (value: boolean) => void; setEnableNetComponents: (value: boolean) => void; setEnableNotebookView: (value: boolean) => void; - - setEnableCodeEditorWorkspace: (value: boolean) => void; - setCodeEditorPlacement: (value: CodeEditorPlacement) => void; setShowWalkthroughOnInit: (value: boolean) => void; setWebGpuEnabled: (value: boolean) => void; setShowCompilationOutput: (value: boolean) => void; @@ -175,9 +168,6 @@ export const defaultUserSettings: UserSettings = { partialSelection: true, enableNetComponents: false, enableNotebookView: false, - - enableCodeEditorWorkspace: false, - codeEditorPlacement: "fullscreen", showWalkthroughOnInit: true, webGpuEnabled: false, showCompilationOutput: false, @@ -214,9 +204,6 @@ export const defaultUserSettingsContextValue: UserSettingsContextValue = { setPartialSelection: () => {}, setEnableNetComponents: () => {}, setEnableNotebookView: () => {}, - - setEnableCodeEditorWorkspace: () => {}, - setCodeEditorPlacement: () => {}, setShowWalkthroughOnInit: () => {}, setWebGpuEnabled: () => {}, setShowCompilationOutput: () => {}, diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx index b28894d4ab4..0f6ae45762d 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.test.tsx @@ -170,37 +170,3 @@ describe("UserSettingsProvider", () => { expect(screen.getByRole("button", { name: "editor: on" })).toBeTruthy(); }); }); - -const CodeLayoutProbe = () => { - const settings = use(UserSettingsContext); - return ( - - ); -}; - -it("remembers the code editor flag and placement across sessions", () => { - const view = render( - - - , - ); - fireEvent.click(screen.getByRole("button", { name: "inline" })); - expect(screen.getByRole("button", { name: "bottom" })).toBeTruthy(); - view.unmount(); - render( - - - , - ); - expect(screen.getByRole("button", { name: "bottom" })).toBeTruthy(); -}); diff --git a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx index d919dc5a35b..7a9eb8bbc59 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx @@ -15,7 +15,6 @@ import type { } from "./editor-context"; import type { ArcRendering, - CodeEditorPlacement, SubViewSectionSettings, UserSettings, } from "./user-settings-context"; @@ -62,11 +61,6 @@ const loadSettings = (): UserSettings => { return { ...defaultUserSettings, ...parsed, - codeEditorPlacement: - parsed.codeEditorPlacement === "properties" || - parsed.codeEditorPlacement === "bottom" - ? parsed.codeEditorPlacement - : "fullscreen", // Someone who had selected the GPU globally keeps it available. webGpuEnabled: parsed.webGpuEnabled ?? computeBackend === "webgpu", }; @@ -136,13 +130,6 @@ const OwnedUserSettingsProvider: React.FC = ({ setState((prev) => ({ ...prev, partialSelection: value })), setEnableNetComponents: (value: boolean) => setState((prev) => ({ ...prev, enableNetComponents: value })), - setEnableCodeEditorWorkspace: (value: boolean) => - setState((previous) => ({ - ...previous, - enableCodeEditorWorkspace: value, - })), - setCodeEditorPlacement: (value: CodeEditorPlacement) => - setState((previous) => ({ ...previous, codeEditorPlacement: value })), setEnableNotebookView: (value: boolean) => setState((prev) => ({ ...prev, enableNotebookView: value })), setShowWalkthroughOnInit: (value: boolean) => diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx deleted file mode 100644 index c3a71cbe8a6..00000000000 --- a/libs/@hashintel/petrinaut/src/ui/monaco/code-editor-layout-settings.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { use } from "react"; - -import { Toggle } from "@hashintel/ds-components"; -import { css } from "@hashintel/ds-helpers/css"; - -import { UserSettingsContext } from "../../react/state/user-settings-context"; - -const layouts = [ - { - value: "fullscreen", - label: "Full screen", - description: "Replace the canvas", - }, - { - value: "properties", - label: "Properties panel", - description: "Keep the canvas beside code", - }, - { - value: "bottom", - label: "Bottom dock", - description: "Keep the canvas above code", - }, -] as const; - -const headingStyle = css({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", - fontSize: "sm", - fontWeight: "medium", - color: "neutral.fg.heading", - gap: "2", -}); -const descriptionStyle = css({ - fontSize: "xs", - color: "neutral.fg.subtle", - marginTop: "1", - marginBottom: "3", - lineHeight: "[1.5]", -}); -const gridStyle = css({ - display: "grid", - gridTemplateColumns: "[repeat(3, minmax(0, 1fr))]", - gap: "2", - marginBottom: "3", -}); -const cardStyle = css({ - padding: "2", - borderWidth: "thin", - borderColor: "neutral.bd.subtle", - borderRadius: "lg", - textAlign: "left", - cursor: "pointer", - background: "neutral.s00", - fontSize: "xs", - color: "neutral.fg.heading", - _hover: { background: "bg.subtle" }, - _focusVisible: { outline: "[2px solid #2563eb]", outlineOffset: "[2px]" }, - "&[aria-pressed=true]": { borderColor: "[#3b82f6]", background: "[#eff6ff]" }, -}); -const previewStyle = css({ - height: "[60px]", - position: "relative", - overflow: "hidden", - borderRadius: "sm", - borderWidth: "thin", - borderColor: "neutral.bd.subtle", - background: "[#f5f6f8]", - marginBottom: "2", -}); -const barStyle = css({ - position: "absolute", - inset: "[0 0 auto]", - height: "[7px]", - background: "[#e2e5eb]", -}); -const sidebarStyle = css({ - position: "absolute", - top: "[7px]", - bottom: "[0]", - width: "[16%]", - borderRight: "[1px solid #e2e5eb]", - background: "neutral.s00", -}); -const codeStyle = css({ - position: "absolute", - padding: "[4px]", - background: "neutral.s00", - border: "[1px solid #c6d9fa]", - color: "[#4d7cc0]", - fontSize: "[8px]", - lineHeight: "[9px]", - fontFamily: "[monospace]", - whiteSpace: "pre", - overflow: "hidden", -}); -const labelStyle = css({ fontWeight: "medium", lineHeight: "[1.3]" }); - -export const CodeEditorLayoutSettings = () => { - const { - enableCodeEditorWorkspace, - setEnableCodeEditorWorkspace, - codeEditorPlacement, - setCodeEditorPlacement, - } = use(UserSettingsContext); - return ( - <> -
- Code editor layouts - -
-

- Open code directly from a field’s menu or the canvas Code menu. Choose - where it opens. -

-
- {layouts.map((layout) => ( - - ))} -
- - ); -}; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx index 361c1f0af80..50dda0b844b 100644 --- a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.stories.tsx @@ -1,4 +1,4 @@ -import { use, useState } from "react"; +import { use } from "react"; import { userEvent, within } from "storybook/test"; import { css } from "@hashintel/ds-helpers/css"; @@ -8,28 +8,21 @@ import { UserSettingsContext } from "../../react/state/user-settings-context"; import { UserSettingsProvider } from "../../react/state/user-settings-provider"; import { PetrinautStoryProvider } from "../petrinaut-story-provider"; -import type { CodeEditorPlacement } from "../../react/state/user-settings-context"; +import type { CodeEditorPlacement } from "./code-workspace"; import type { Meta, StoryObj } from "@storybook/react-vite"; const LayoutExample = ({ - placement, readonly = false, }: { placement: CodeEditorPlacement; readonly?: boolean; }) => { const settings = use(UserSettingsContext); - const [enabled, setEnabled] = useState(true); - const [layout, setLayout] = useState(placement); return (
@@ -54,28 +47,38 @@ const meta = { ), ], - play: async ({ canvasElement }) => { + play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); await userEvent.click( await canvas.findByRole( - "button", - { name: "Open code" }, + "option", + { name: /^Production Success/ }, { timeout: 15000 }, ), ); - const page = within(canvasElement.ownerDocument.body); - await userEvent.click( - await page.findByRole("menuitem", { - name: "Production Success / Transition kernel", - }), - ); + const resultsToggle = await canvas.findByRole("button", { + name: /Transition Results/, + }); + if (resultsToggle.getAttribute("aria-expanded") !== "true") { + await userEvent.click(resultsToggle); + } + if (args.placement === "fullscreen") { + const section = canvasElement.querySelector( + '[id="transition-results"]', + ); + if (!section) throw new Error("Transition results section is missing"); + await userEvent.click( + await within(section).findByRole("button", { + name: /Full screen/, + }), + ); + } }, } satisfies Meta; export default meta; type Story = StoryObj; export const FullScreen: Story = { args: { placement: "fullscreen" } }; export const PropertiesPanel: Story = { args: { placement: "properties" } }; -export const BottomDock: Story = { args: { placement: "bottom" } }; export const ReadOnly: Story = { args: { placement: "properties", readonly: true }, }; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx index 070feff5347..c6a203eeffa 100644 --- a/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-workspace.test.tsx @@ -1,6 +1,6 @@ /** @vitest-environment jsdom */ import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { use, useState } from "react"; +import { use } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_PETRINAUT_EXTENSIONS } from "@hashintel/petrinaut-core"; @@ -13,10 +13,16 @@ import { defaultUserSettingsContextValue, UserSettingsContext, } from "../../react/state/user-settings-context"; -import { CodeWorkspaceProvider, useCodeWorkspace } from "./code-workspace"; +import { PetrinautPresentationProvider } from "../views/shared/presentation-context"; +import { + CodeWorkspacePanel, + CodeWorkspaceProvider, + SourceCodeEditor, + useCodeWorkspace, +} from "./code-workspace"; import { getCodeEntries } from "./code-workspace/entries"; -import type { CodeEditorPlacement } from "../../react/state/user-settings-context"; +import type { CodeEditorProps } from "./code-editor"; const mutations = vi.hoisted(() => ({ updatePlace: vi.fn(), @@ -24,6 +30,29 @@ const mutations = vi.hoisted(() => ({ updateDifferentialEquation: vi.fn(), })); vi.mock("../../react", () => ({ usePetrinautMutations: () => mutations })); +vi.mock("./code-editor", () => ({ + CodeEditor: ({ path, value, options, onChange }: CodeEditorProps) => ( +