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..06638161357 --- /dev/null +++ b/.changeset/tidy-code-editor-layouts.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Expand code sections to fill the Properties Panel with smooth transitions, a parent breadcrumb, keyboard focus restoration, and browser history navigation. Make Canvas and Definitions available as Edit views with browser history navigation. Preserve the latest code when switching between Edit views. diff --git a/apps/petrinaut-website/src/examples/example-search.test.ts b/apps/petrinaut-website/src/examples/example-search.test.ts index 6d53a5045ca..5f241d46c71 100644 --- a/apps/petrinaut-website/src/examples/example-search.test.ts +++ b/apps/petrinaut-website/src/examples/example-search.test.ts @@ -8,6 +8,26 @@ import { } from "./example-search"; describe("example search contract", () => { + it("carries expanded sections as complete panel and section pairs", () => { + const expanded = { + expandedPanel: "transition-properties", + expandedSection: "transition-results", + }; + expect(validateSharedExampleSearch(expanded)).toMatchObject(expanded); + expect(canonicalSearchString(expanded)).toBe( + "expandedPanel=transition-properties&expandedSection=transition-results", + ); + for (const input of [ + { expandedPanel: "transition-properties" }, + { expandedSection: "transition-results" }, + { expandedPanel: 42, expandedSection: "transition-results" }, + { expandedPanel: "transition-properties", expandedSection: "" }, + ]) { + const search = validateSharedExampleSearch(input); + expect(search.expandedPanel).toBeUndefined(); + expect(search.expandedSection).toBeUndefined(); + } + }); it("validates settings sections only for the user settings dialog", () => { expect( validateSharedExampleSearch({ diff --git a/apps/petrinaut-website/src/examples/example-search.ts b/apps/petrinaut-website/src/examples/example-search.ts index b245df20796..9f460f4756c 100644 --- a/apps/petrinaut-website/src/examples/example-search.ts +++ b/apps/petrinaut-website/src/examples/example-search.ts @@ -22,7 +22,9 @@ import { * `navigation-search.ts` maps each of these onto the editor's own vocabulary * with an exhaustive switch, so a rename on either side fails to compile. */ -export const sharedModes = ["edit", "simulate", "actual", "notebook"] as const; +export const sharedModes = ["edit", "simulate", "actual"] as const; + +export const sharedEditViews = ["canvas", "definitions"] as const; export const sharedSimulateViews = [ "scenarios", @@ -45,6 +47,7 @@ export const sharedSettingsSections = [ "labs", ] as const; +export type SharedEditView = (typeof sharedEditViews)[number]; export type SharedMode = (typeof sharedModes)[number]; export type SharedSimulateView = (typeof sharedSimulateViews)[number]; export type SharedOverlay = (typeof sharedOverlays)[number]; @@ -64,9 +67,12 @@ export type SharedExampleSearch = { itemType?: SelectionItemType; itemId?: string; mode?: SharedMode; + editView?: SharedEditView; view?: SharedSimulateView; overlay?: SharedOverlay; settings?: (typeof sharedSettingsSections)[number]; + expandedPanel?: string; + expandedSection?: string; }; /** The keys this contract owns. Anything else in a URL is foreign. */ @@ -76,9 +82,12 @@ const sharedSearchKeys = [ "itemType", "itemId", "mode", + "editView", "view", "overlay", "settings", + "expandedPanel", + "expandedSection", ] as const satisfies readonly (keyof SharedExampleSearch)[]; // `.catch(undefined)` is the contract's whole validation story: anything a URL @@ -90,6 +99,7 @@ const optionalSelectionItemType = z .optional() .catch(undefined); +const optionalEditView = z.enum(sharedEditViews).optional().catch(undefined); const optionalMode = z.enum(sharedModes).optional().catch(undefined); const optionalSimulateView = z .enum(sharedSimulateViews) @@ -124,9 +134,21 @@ export const validateSharedExampleSearch = ( ): SharedExampleSearch => ({ scenario: optionalNonEmptyString.parse(input.scenario), subnet: optionalNonEmptyString.parse(input.subnet), - mode: optionalMode.parse(input.mode), + mode: input.mode === "notebook" ? "edit" : optionalMode.parse(input.mode), + editView: + input.mode === "notebook" + ? "definitions" + : optionalEditView.parse( + input.editView === "notebook" ? "definitions" : input.editView, + ), view: optionalSimulateView.parse(input.view), overlay: optionalOverlay.parse(input.overlay), + expandedPanel: optionalNonEmptyString.parse(input.expandedSection) + ? optionalNonEmptyString.parse(input.expandedPanel) + : undefined, + expandedSection: optionalNonEmptyString.parse(input.expandedPanel) + ? optionalNonEmptyString.parse(input.expandedSection) + : undefined, settings: input.overlay === "user-settings" ? z diff --git a/apps/petrinaut-website/src/examples/navigation-search.test.ts b/apps/petrinaut-website/src/examples/navigation-search.test.ts index b691d6e1b5b..a65302040a3 100644 --- a/apps/petrinaut-website/src/examples/navigation-search.test.ts +++ b/apps/petrinaut-website/src/examples/navigation-search.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; -import { sharedOverlays, sharedSimulateViews } from "./example-search"; +import { + sharedOverlays, + sharedSimulateViews, + validateSharedExampleSearch, +} from "./example-search"; import { applyPreviewNavigationUpdate, navigationStateToSharedSearch, @@ -8,6 +12,64 @@ import { } from "./navigation-search"; describe("navigation state projection", () => { + it("routes Definitions within Edit and preserves the selection in shared links", () => { + const search = { + editView: "definitions", + itemType: "transition", + itemId: "collision", + } as const; + const state = sharedSearchToNavigationState(search); + expect(state.mode).toBe("edit"); + expect(state.editView).toBe("definitions"); + expect(navigationStateToSharedSearch(state)).toMatchObject(search); + expect( + applyPreviewNavigationUpdate(search, (current) => current), + ).toMatchObject(search); + expect(sharedSearchToNavigationState({}).editView).toBe("canvas"); + }); + + it.each([{ mode: "notebook" }, { editView: "notebook" }])( + "opens legacy Notebook links in the Edit workspace: %s", + (legacy) => { + const search = validateSharedExampleSearch({ + ...legacy, + itemType: "place", + itemId: "space", + }); + expect(search).toMatchObject({ + editView: "definitions", + itemId: "space", + }); + expect(sharedSearchToNavigationState(search)).toMatchObject({ + mode: "edit", + editView: "definitions", + }); + }, + ); + + it("round-trips an expanded Properties Panel section with its selected item", () => { + const search = { + itemType: "transition", + itemId: "collision", + expandedPanel: "transition-properties", + expandedSection: "transition-results", + } as const; + const state = sharedSearchToNavigationState(search); + expect(state.expandedSubView).toEqual({ + container: search.expandedPanel, + id: search.expandedSection, + }); + expect(navigationStateToSharedSearch(state)).toMatchObject(search); + expect( + sharedSearchToNavigationState({ + itemType: "transition", + itemId: "collision", + }).expandedSubView, + ).toBeNull(); + expect( + applyPreviewNavigationUpdate(search, (current) => current), + ).toMatchObject(search); + }); it.each(["general", "viewport", "simulation", "labs"] as const)( "round-trips the %s settings section in Simulate", (settings) => { diff --git a/apps/petrinaut-website/src/examples/navigation-search.ts b/apps/petrinaut-website/src/examples/navigation-search.ts index 7a009ee4148..b03845a3807 100644 --- a/apps/petrinaut-website/src/examples/navigation-search.ts +++ b/apps/petrinaut-website/src/examples/navigation-search.ts @@ -2,9 +2,9 @@ * Projects the example URL contract onto Petrinaut's navigation state. * * The URL carries the location a reader can act on: the scenario, the subnet, - * the focused item, the editor's mode, its Simulate section and the overlay it - * has open. It deliberately leaves out `simulateResource`, which names a run - * or a record inside the open document rather than a place in the app. + * the focused item, its expanded properties section, the editor's mode, its + * Simulate section and the overlay it has open. It leaves out `simulateResource`, + * which names a run or a record inside the open document rather than a place in the app. * * Every field is decoded against a BASELINE — the location its page starts * from. A URL that does not name a field means "the baseline's value", which is @@ -18,6 +18,7 @@ import { selectionFromInput, selectionToSearch, type SharedExampleSearch, + type SharedEditView, type SharedMode, type SharedOverlay, type SharedSimulateView, @@ -26,6 +27,7 @@ import { import type { PetrinautPreviewNavigationState } from "@hashintel/petrinaut/preview"; import type { EditorGlobalMode, + EditViewMode, PetrinautNavigationOverlay, PetrinautNavigationState, PetrinautNavigationUpdater, @@ -60,6 +62,8 @@ const scenarioToSearch = ( * editor fails this file's type check until the contract decides whether the * URL should carry it. */ +const editViewToSearch = (view: EditViewMode): SharedEditView => view; + const modeToSearch = (mode: EditorGlobalMode): SharedMode => mode; const simulateViewToSearch = (view: SimulateViewMode): SharedSimulateView => @@ -84,6 +88,11 @@ export const sharedSearchToNavigationState = ( subnetId: search.subnet ?? null, selection: selectionFromInput(search as Record), mode: search.mode ?? baseline.mode, + editView: search.editView ?? baseline.editView, + expandedSubView: + search.expandedPanel && search.expandedSection + ? { container: search.expandedPanel, id: search.expandedSection } + : null, simulateView: search.view ?? baseline.simulateView, overlay: search.overlay === undefined @@ -96,14 +105,19 @@ export const navigationStateToSharedSearch = ( baseline: PetrinautNavigationState = defaultPetrinautNavigationState, ): SharedExampleSearch => { const mode = modeToSearch(state.mode); + const editView = editViewToSearch(state.editView); const view = simulateViewToSearch(state.simulateView); const overlay = overlayToSearch(state.overlay); return { scenario: scenarioToSearch(state.scenarioId), subnet: state.subnetId ?? undefined, + expandedPanel: state.expandedSubView?.container, + expandedSection: state.expandedSubView?.id, // Omitted at the baseline, so an untouched page keeps a clean URL and the // decode above puts the baseline back. mode: mode === modeToSearch(baseline.mode) ? undefined : mode, + editView: + editView === editViewToSearch(baseline.editView) ? undefined : editView, view: view === simulateViewToSearch(baseline.simulateView) ? undefined : view, overlay: @@ -143,9 +157,12 @@ export const applyPreviewNavigationUpdate = ( update: PetrinautNavigationUpdater, ): SharedExampleSearch => ({ mode: search.mode, + editView: search.editView, view: search.view, overlay: search.overlay, settings: search.settings, + expandedPanel: search.expandedPanel, + expandedSection: search.expandedSection, ...navigationStateToPreviewSearch( update(previewSearchToNavigationState(search)), ), diff --git a/apps/petrinaut-website/src/examples/use-shared-search-navigation.test.tsx b/apps/petrinaut-website/src/examples/use-shared-search-navigation.test.tsx index 36812ea7ae7..72f42406b5d 100644 --- a/apps/petrinaut-website/src/examples/use-shared-search-navigation.test.tsx +++ b/apps/petrinaut-website/src/examples/use-shared-search-navigation.test.tsx @@ -3,7 +3,10 @@ import { act, render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import { useSharedSearchNavigation } from "./use-shared-search-navigation"; +import { + useSharedSearchNavigation, + withClearedSharedLocation, +} from "./use-shared-search-navigation"; import type { SharedExampleSearch } from "./example-search"; import type { @@ -34,6 +37,104 @@ const Probe = ({ }; describe("useSharedSearchNavigation", () => { + it("pushes Edit view switches and follows Back and Forward without changing selection", () => { + let controller!: PetrinautNavigationController; + const onSearchChange = vi.fn(); + const parent = { itemType: "transition", itemId: "collision" } as const; + const notebook = { ...parent, editView: "definitions" } as const; + const onController = (value: PetrinautNavigationController) => { + controller = value; + }; + const props = { onController, onSearchChange }; + const view = render(); + act(() => + controller.onNavigate( + (current) => ({ ...current, editView: "definitions" }), + { history: "push", intent: { cause: "user", action: "edit-view" } }, + ), + ); + expect(onSearchChange).toHaveBeenCalledWith( + expect.objectContaining(notebook), + "push", + ); + view.rerender(); + view.rerender(); + expect(controller.state.editView).toBe("canvas"); + view.rerender(); + expect(controller.state.editView).toBe("definitions"); + expect(controller.state.mode).toBe("edit"); + expect(controller.state.selection).toEqual([ + { type: "transition", id: "collision" }, + ]); + expect(onSearchChange).toHaveBeenCalledOnce(); + }); + + it("writes section expansion to history and follows Back, Forward, and document replacement", () => { + let controller!: PetrinautNavigationController; + const onSearchChange = vi.fn(); + const parent = { itemType: "transition", itemId: "collision" } as const; + const expanded = { + ...parent, + expandedPanel: "transition-properties", + expandedSection: "transition-results", + }; + const onController = (value: PetrinautNavigationController) => { + controller = value; + }; + const view = render( + , + ); + act(() => + controller.onNavigate( + (current) => ({ + ...current, + expandedSubView: { + container: "transition-properties", + id: "transition-results", + }, + }), + { history: "push", intent: { cause: "user", action: "subview" } }, + ), + ); + expect(onSearchChange).toHaveBeenCalledWith( + expect.objectContaining(expanded), + "push", + ); + view.rerender( + , + ); + view.rerender( + , + ); + expect(controller.state.expandedSubView).toBeNull(); + view.rerender( + , + ); + expect(controller.state.expandedSubView).toEqual({ + container: "transition-properties", + id: "transition-results", + }); + expect(onSearchChange).toHaveBeenCalledOnce(); + expect( + withClearedSharedLocation(controller.state).expandedSubView, + ).toBeNull(); + }); it("keeps URL-unrepresentable state in memory and mirrors the shared subset", () => { let controller!: PetrinautNavigationController; const onSearchChange = vi.fn(); diff --git a/apps/petrinaut-website/src/examples/use-shared-search-navigation.ts b/apps/petrinaut-website/src/examples/use-shared-search-navigation.ts index f3e0b7de177..47d6f0064f8 100644 --- a/apps/petrinaut-website/src/examples/use-shared-search-navigation.ts +++ b/apps/petrinaut-website/src/examples/use-shared-search-navigation.ts @@ -35,7 +35,9 @@ const mergeSharedSearch = ( scenarioId: shared.scenarioId, subnetId: shared.subnetId, selection: shared.selection, + expandedSubView: shared.expandedSubView, mode: shared.mode, + editView: shared.editView, simulateView: shared.simulateView, overlay: shared.overlay, }; @@ -58,13 +60,14 @@ export const withClearedSharedLocation = ( scenarioId: undefined, subnetId: null, selection: [], + expandedSubView: null, }); /** * Navigation controller for pages whose URL carries the shared location: the - * scenario, the subnet, the focused item, the mode, the Simulate section and - * the open overlay. The editor navigates one field more than that — the - * resource open inside Simulate — so the full location still lives in page + * scenario, the subnet, the focused item, its expanded properties section, + * the mode, the Simulate section and the open overlay. The editor also navigates + * the resource open inside Simulate, so the full location still lives in page * state and only its shared projection reaches the URL. * * `initialState` is the location this page starts from, for every field the URL diff --git a/libs/@hashintel/petrinaut-core/src/ai.ts b/libs/@hashintel/petrinaut-core/src/ai.ts index 894cb7126d7..83a818a15e3 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": + "Code editing in the Properties Panel: expand a section to fill the panel, return to the item’s other properties, direct function navigation, 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..59c0fa6f7da 100644 --- a/libs/@hashintel/petrinaut/docs/README.md +++ b/libs/@hashintel/petrinaut/docs/README.md @@ -23,13 +23,13 @@ A quick map of the things you'll encounter: Petrinaut has three global modes in the top bar, though **Actual** is only enabled when the host application provides a live execution source: -- **Edit** -- the drawing/configuration workspace plus single-run simulation playback. +- **Edit** -- Canvas and Definitions views for editing the same net, plus single-run simulation playback on Canvas. - **Simulate** -- a separate management surface for scenarios and experiments. - **Actual** -- a read-only live-execution view supplied by a host such as Brunch. ## Contents -- [Drawing a Net](drawing-a-net.md) -- Top bar, canvas, sidebars, adding nodes and connecting arcs, selection, keyboard shortcuts, import/export, auto-layout. +- [Drawing a Net](drawing-a-net.md) -- Top bar, Canvas and Definitions views, sidebars, adding nodes and connecting arcs, selection, keyboard shortcuts, import/export, auto-layout. - [Petri Net Extensions](petri-net-extensions.md) -- Types, dynamics, transition kernels, firing rules, read/inhibitor arcs, as well as parameters and state visualizers. - [Useful Patterns](useful-patterns.md) -- Common modelling techniques, including duration and resource pools. - [Simulation](simulation.md) -- Set initial state, run a single simulation, use the timeline, control playback. @@ -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) -- Edit model functions and expand their sections within the Properties Panel. - [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/ai-assistant.md b/libs/@hashintel/petrinaut/docs/ai-assistant.md index 93cd4c39eb0..1913dea1efc 100644 --- a/libs/@hashintel/petrinaut/docs/ai-assistant.md +++ b/libs/@hashintel/petrinaut/docs/ai-assistant.md @@ -11,7 +11,7 @@ Open the assistant in any of these ways: 3. **First-run prompt**. When you load Petrinaut against an empty net, a centred prompt appears. Type a description and its trailing action becomes **Send**; select it to open the panel with your message already in flight. When the host provides Voice mode, the empty prompt instead shows a waveform action titled **Start voice mode**. It opens the same assistant without creating an empty text message. Dismiss the prompt with the **X**, by clicking outside it, or by pressing **Escape**; it is hidden for the rest of the session once dismissed. 4. **Command palette**. Choose **Toggle AI assistant**, or press **Cmd/Ctrl+Shift+K** directly. This opens the assistant and focuses the message field, or closes it when already visible, including compact Voice mode. Reopening preserves the conversation and docked or floating layout. The command keeps you in your current view. This command is available when the host provides an AI assistant. -The assistant stays available across **Edit**, **Simulate**, **Actual**, and **Notebook** modes. Switching views preserves your conversation, draft, and active response. The panel resizes by dragging its left edge. Text and voice share the primary transcript. Hosts can name that tab and add a second tab for related content. Select a tab to switch views, or use the left/right arrow keys while a tab is focused. Switching does not end a response, clear your draft or interrupt Voice; the composer and active controls remain available. +The assistant stays available across **Edit** (Canvas or Definitions), **Simulate**, and **Actual** modes. Switching views preserves your conversation, draft, and active response. The panel resizes by dragging its left edge. Text and voice share the primary transcript. Hosts can name that tab and add a second tab for related content. Select a tab to switch views, or use the left/right arrow keys while a tab is focused. Switching does not end a response, clear your draft or interrupt Voice; the composer and active controls remain available. Some hosts mark activity that happened in the tab you are not viewing. A numbered badge counts unseen host-content updates; viewing that tab acknowledges them. A dot on the transcript tab means the conversation completed, errored, or was stopped while you were viewing the host tab; returning to the transcript acknowledges it. These acknowledgements are panel state, not durable conversation or document records, and reset when the mounted conversation is replaced. diff --git a/libs/@hashintel/petrinaut/docs/code-editor.md b/libs/@hashintel/petrinaut/docs/code-editor.md new file mode 100644 index 00000000000..d84aefedeb4 --- /dev/null +++ b/libs/@hashintel/petrinaut/docs/code-editor.md @@ -0,0 +1,32 @@ +# Code Editor + +Edit model functions in the Properties Panel. Expand a code section to fill the panel while keeping its controls available. + +## Open code + +Select a place, transition, or differential equation in the canvas or left sidebar. Open its code section in the Properties Panel: + +- **Firing Time** contains a transition’s predicate or stochastic rate. +- **Transition Results** contains a transition’s output function. +- **Differential Equation** contains the equation’s properties and code. +- **Visualizer** contains a place’s visualization code and preview. + +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. + +## Fill the panel + +Use the **Fill panel** button in the section header to expand that section to the full width and height of the Properties Panel’s section area. Its controls stay beside the code. The canvas and the panel’s width stay unchanged. + +The expanded header shows a breadcrumb, such as **Transition Collision › Transition Results**, using the parent’s usual icon and title. Long parent names fade so the code section’s name stays visible. Click the parent name to return to the item’s other properties. Code, undo history, cursor position, and the other sections’ sizes and contents remain in place. + +Expanding and returning animate within the section container. These transitions respect the animation setting and your system’s reduced-motion preference. + +On hosts with app navigation enabled, expanding a section updates the URL and browser history. **Back** closes the expanded view and **Forward** reopens it. Reloading or opening that URL restores the selected item and expanded section. + +Returning restores keyboard focus to **Fill panel**, or to the section header if the section was collapsed before opening the link. The section keeps its saved collapsed state. + +Drag the panel’s left edge to change its width. After returning to the other properties, drag the dividers between sections to change their heights. + +Changes apply automatically to the model. Read-only files and simulation modes keep code read-only. Selecting another item or changing the file or active subnet restores the usual section layout. + +The Properties Panel and Definitions show the same saved code. Switching between them keeps your latest edits. diff --git a/libs/@hashintel/petrinaut/docs/drawing-a-net.md b/libs/@hashintel/petrinaut/docs/drawing-a-net.md index e1336946568..a5131580fbe 100644 --- a/libs/@hashintel/petrinaut/docs/drawing-a-net.md +++ b/libs/@hashintel/petrinaut/docs/drawing-a-net.md @@ -5,6 +5,7 @@ The editor is organized around a central canvas where you build your net: - **Top bar** -- net management menu, optional title field, **Edit / Simulate / Actual** mode switcher, active-experiments indicator, recent-changes history. See [Top bar](#top-bar). +- **Canvas / Definitions** -- a compact selector floating over Canvas, or beside search in the Definitions toolbar. - **Canvas** (center) -- the main workspace where places and transitions are displayed and connected. - **Left sidebar** -- the Entities tree: every node, token type, differential equation and parameter in the net, in one hierarchy. See [Left sidebar](#left-sidebar). - **Properties panel** (right) -- opens when you select an entity, showing its configurable properties. @@ -44,11 +45,11 @@ Spans the full editor width and has three sections. Petrinaut global modes are switched via the centre control in the top bar. -| Mode | Workspace | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Edit** | Canvas + left sidebar + properties panel + bottom panel + bottom toolbar (with AI assistant). This is where you draw the net, configure entities, write code, and run single simulations. | -| **Simulate** | Replaces the workspace with the [Scenarios](scenarios.md) and [Experiments](experiments.md) management views. | -| **Actual** | Shows a host-provided live execution source. It is disabled unless the host provides Actual-mode data. See [Actual Mode](actual-mode.md). | +| Mode | Workspace | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Edit** | Canvas or Definitions. Draw and connect nodes on Canvas, or edit the same net as expandable cells in Definitions. Canvas also provides single-run simulation controls. | +| **Simulate** | Replaces the workspace with the [Scenarios](scenarios.md) and [Experiments](experiments.md) management views. | +| **Actual** | Shows a host-provided live execution source. It is disabled unless the host provides Actual-mode data. See [Actual Mode](actual-mode.md). | In Simulate mode the net structure becomes read-only -- you can still manage scenarios and experiments. You cannot change places, transitions, arcs, types, or parameters. Switch back to Edit mode to modify the net. @@ -56,6 +57,30 @@ In Actual mode the net is also read-only. It shows the Petri net supplied by the Switching modes does not stop background experiments. The active-experiments indicator remains visible in the top bar from any mode. +## Canvas and Definitions + +In **Edit**, use **Canvas / Definitions** to switch views. On Canvas, the selector +floats below the main header, beside the Entities sidebar when it is open. +In Definitions, it shares the search and filter toolbar. The compact selector +becomes fully opaque when hovered or focused. Both views work on +the same net and share the selected entity. +Switching between them keeps the canvas position and zoom, and the Definitions view’s +scroll position, expanded cells, search, and filters. + +**Canvas** shows the diagram, sidebars, Properties Panel, and simulation +controls. Use it to add or remove nodes and arcs and to arrange the net. + +**Definitions** lists places, transitions, types, differential equations, and +parameters as expandable cells. Open a cell to edit its name, fields, arc +weights, type assignments, or code. Use the search and kind filters to narrow +the list, or switch its order between document and dependency order. The graph +on the right shows how the selected entity connects to the rest of the net. + +On the website, the URL +records the selected view; Back and Forward switch views, and reloading a +Definitions link opens Definitions again. Simulate and Actual keep their own +workspaces. + ## Adding places and transitions Use the bottom toolbar to add nodes: diff --git a/libs/@hashintel/petrinaut/docs/visual-settings.md b/libs/@hashintel/petrinaut/docs/visual-settings.md index f5f858be507..c2e92db82fc 100644 --- a/libs/@hashintel/petrinaut/docs/visual-settings.md +++ b/libs/@hashintel/petrinaut/docs/visual-settings.md @@ -2,7 +2,7 @@ Open **User settings** from the menu in the top-left corner, press **Cmd+,** on macOS or **Ctrl+,** on Windows/Linux, or run **Open user settings** in the Command Palette. -Settings are available from every workspace tab, including **Simulate → Experiments**, **Actual**, and **Notebook** when those modes are available. The **gear icon** in the bottom-right viewport controls opens the **Viewport** section directly. +Settings are available in **Edit** (Canvas or Definitions), **Simulate**, and **Actual** when that mode is available. The **gear icon** in the bottom-right viewport controls opens the **Viewport** section directly. Use the vertical tabs to browse the sections. With a tab focused, use **Up/Down** to switch sections and **Home/End** to jump to the first or last section. **Right** moves into the section's settings, **Up/Down** moves between controls, and **Left** returns to the selected tab. Moving back into the same section restores the last focused control. **Tab** and **Shift+Tab** also reach every control. Use **Space** to toggle a switch and **Enter** to open a dropdown; an open dropdown uses its own arrow keys. Press **Escape**, click the close button, or click outside the dialog to close it. @@ -179,10 +179,6 @@ Shown only when the host application provides an optimizer that runs in your bro ## Labs -### Notebook view (experimental) - -Add a **Notebook** tab to explore the net as expandable, read-only cells. Off by default. - ### Net Components (experimental) Enable subnet definitions and component instances for hierarchical nets. This option appears when the net supports subnets. Off by default. diff --git a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx index 9e2ca5d5bf7..5e59a18326d 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx @@ -40,6 +40,7 @@ const editorContextValue = ( globalMode, navigateTo: () => {}, setGlobalMode: () => {}, + setEditViewMode: () => {}, setEditionMode: () => {}, setAddComponentMode: () => {}, setCursorMode: () => {}, diff --git a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx index 03edb94f52d..3f3f9f15483 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx @@ -43,6 +43,7 @@ const editorContextValue = ( globalMode, navigateTo: () => {}, setGlobalMode: () => {}, + setEditViewMode: () => {}, setEditionMode: () => {}, setAddComponentMode: () => {}, setCursorMode: () => {}, diff --git a/libs/@hashintel/petrinaut/src/react/index.ts b/libs/@hashintel/petrinaut/src/react/index.ts index 257c0f14a11..4159485c918 100644 --- a/libs/@hashintel/petrinaut/src/react/index.ts +++ b/libs/@hashintel/petrinaut/src/react/index.ts @@ -63,6 +63,7 @@ export type { // either gains a member. export type { EditorGlobalMode, + EditViewMode, SimulateViewMode, } from "./state/editor-context"; export { diff --git a/libs/@hashintel/petrinaut/src/react/navigation/index.test.tsx b/libs/@hashintel/petrinaut/src/react/navigation/index.test.tsx index 5f6129a882c..1ae59c12914 100644 --- a/libs/@hashintel/petrinaut/src/react/navigation/index.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/navigation/index.test.tsx @@ -21,6 +21,64 @@ import type { } from "."; describe("Petrinaut navigation", () => { + test("clears an expanded SubView when its item or subnet changes", () => { + const initialState: PetrinautNavigationState = { + ...defaultPetrinautNavigationState, + selection: [{ type: "transition", id: "collision" }], + expandedSubView: { + container: "transition-properties", + id: "transition-results", + }, + }; + const onNavigate = vi.fn(); + const Probe = () => { + const { navigate } = usePetrinautNavigation(); + return ( + <> + + + + ); + }; + render( + + + , + ); + fireEvent.click( + screen.getByRole("button", { name: "Select another transition" }), + ); + expect( + onNavigate.mock.calls.at(-1)?.[0](initialState).expandedSubView, + ).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Open subnet" })); + expect( + onNavigate.mock.calls.at(-1)?.[0](initialState).expandedSubView, + ).toBeNull(); + }); + test("distinguishes settings sections and normalizes the default section", () => { const state = ( section?: "general" | "viewport", diff --git a/libs/@hashintel/petrinaut/src/react/navigation/index.tsx b/libs/@hashintel/petrinaut/src/react/navigation/index.tsx index 9757d6bb29e..97a9badf15e 100644 --- a/libs/@hashintel/petrinaut/src/react/navigation/index.tsx +++ b/libs/@hashintel/petrinaut/src/react/navigation/index.tsx @@ -19,6 +19,7 @@ import { ActualModeContext } from "../actual-mode-context"; import type { EditorGlobalMode, + EditViewMode, SimulateDrawerState, SimulateViewMode, } from "../state/editor-context"; @@ -52,21 +53,25 @@ export type PetrinautNavigationOverlay = */ export type PetrinautNavigationState = { mode: EditorGlobalMode; + editView: EditViewMode; simulateView: SimulateViewMode; simulateResource: PetrinautSimulateResource | null; scenarioId: string | null | undefined; subnetId: string | null; selection: readonly SelectionItem[]; + expandedSubView: { container: string; id: string } | null; overlay: PetrinautNavigationOverlay; }; export const defaultPetrinautNavigationState: PetrinautNavigationState = { mode: "edit", + editView: "canvas", simulateView: "experiments", simulateResource: null, scenarioId: undefined, subnetId: null, selection: [], + expandedSubView: null, overlay: null, }; @@ -74,11 +79,13 @@ export type PetrinautNavigationHistory = "push" | "replace"; export type PetrinautNavigationAction = | "mode" + | "edit-view" | "simulation-view" | "simulation-resource" | "scenario" | "subnet" | "selection" + | "subview" | "overlay"; export type PetrinautNavigationIntent = @@ -171,12 +178,15 @@ export const petrinautNavigationStatesMatch = ( right: Readonly, ) => left.mode === right.mode && + left.editView === right.editView && left.simulateView === right.simulateView && left.simulateResource?.type === right.simulateResource?.type && left.simulateResource?.id === right.simulateResource?.id && left.scenarioId === right.scenarioId && left.subnetId === right.subnetId && selectionsMatch(left.selection, right.selection) && + left.expandedSubView?.container === right.expandedSubView?.container && + left.expandedSubView?.id === right.expandedSubView?.id && left.overlay?.type === right.overlay?.type && (left.overlay?.type !== "user-settings" || right.overlay?.type !== "user-settings" || @@ -189,10 +199,18 @@ const resolveNavigationUpdate = ( ): PetrinautNavigationState => { const updated = typeof update === "function" ? update(current) : { ...current, ...update }; + const selection = canonicalizeSelection(updated.selection); + const scopeChanged = + updated.subnetId !== current.subnetId || + !selectionsMatch(selection, current.selection); return { ...updated, - selection: canonicalizeSelection(updated.selection), + selection, + expandedSubView: + scopeChanged && updated.expandedSubView === current.expandedSubView + ? null + : updated.expandedSubView, }; }; diff --git a/libs/@hashintel/petrinaut/src/react/state/editor-context.ts b/libs/@hashintel/petrinaut/src/react/state/editor-context.ts index e2348528477..d36ffb178b4 100644 --- a/libs/@hashintel/petrinaut/src/react/state/editor-context.ts +++ b/libs/@hashintel/petrinaut/src/react/state/editor-context.ts @@ -14,7 +14,8 @@ export type DraggingStateByNodeId = Record< { dragging: boolean; position: { x: number; y: number } } >; -export type EditorGlobalMode = "edit" | "simulate" | "actual" | "notebook"; +export type EditorGlobalMode = "edit" | "simulate" | "actual"; +export type EditViewMode = "canvas" | "definitions"; type EditorEditionMode = | "cursor" | "add-place" @@ -44,6 +45,7 @@ export type SimulateDrawerState = export type EditorNavigationTarget = { globalMode?: EditorGlobalMode; + editViewMode?: EditViewMode; simulateViewMode?: SimulateViewMode; simulateDrawer?: SimulateDrawerState; selection?: SelectionMap; @@ -70,6 +72,7 @@ export type TimelineView = */ export type EditorState = { globalMode: EditorGlobalMode; + editViewMode: EditViewMode; editionMode: EditorEditionMode; cursorMode: CursorMode; isLeftSidebarOpen: boolean; @@ -138,6 +141,7 @@ export type EditorActions = { /** Navigate several editor surfaces as one app-history transition. */ navigateTo: (target: EditorNavigationTarget) => void; setGlobalMode: (mode: EditorGlobalMode) => void; + setEditViewMode: (view: EditViewMode) => void; setEditionMode: (mode: EditorEditionMode) => void; setCursorMode: (mode: CursorMode) => void; setLeftSidebarOpen: (isOpen: boolean) => void; @@ -194,6 +198,7 @@ export type EditorContextValue = EditorState & export const initialEditorState: EditorState = { globalMode: "edit", + editViewMode: "canvas", editionMode: "cursor", cursorMode: "pan", isLeftSidebarOpen: true, @@ -227,6 +232,7 @@ const DEFAULT_CONTEXT_VALUE: EditorContextValue = { ...initialEditorState, navigateTo: () => {}, setGlobalMode: () => {}, + setEditViewMode: () => {}, setEditionMode: () => {}, setCursorMode: () => {}, setLeftSidebarOpen: () => {}, diff --git a/libs/@hashintel/petrinaut/src/react/state/editor-provider.test.tsx b/libs/@hashintel/petrinaut/src/react/state/editor-provider.test.tsx index 6d99845f420..6ac24fa76a2 100644 --- a/libs/@hashintel/petrinaut/src/react/state/editor-provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/editor-provider.test.tsx @@ -188,6 +188,23 @@ describe("EditorProvider selection gestures", () => { ); }); + it("records Edit view switches without changing the mode or selection", () => { + act(() => editor.setSelection(selectionOf("place-a"))); + recorded.length = 0; + act(() => editor.setEditViewMode("definitions")); + expect(editor.globalMode).toBe("edit"); + expect(editor.editViewMode).toBe("definitions"); + expect(editor.selection).toEqual(selectionOf("place-a")); + expect(recorded).toEqual([ + { history: "push", intent: { cause: "user", action: "edit-view" } }, + ]); + act(() => editor.setGlobalMode("simulate")); + act(() => editor.setGlobalMode("edit")); + expect(editor.editViewMode).toBe("definitions"); + act(() => editor.setEditViewMode("canvas")); + expect(editor.selection).toEqual(selectionOf("place-a")); + }); + const flushMicrotasks = () => act(async () => {}); it("coalesces react-flow batched updates and records one entry per gesture", async () => { diff --git a/libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx b/libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx index 17a15aeeb52..2643a61d4fd 100644 --- a/libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx @@ -294,6 +294,9 @@ export const EditorProvider: React.FC = ({ children }) => { (current) => ({ ...current, ...(target.globalMode !== undefined ? { mode: target.globalMode } : {}), + ...(target.editViewMode !== undefined + ? { editView: target.editViewMode } + : {}), ...(target.simulateViewMode !== undefined ? { simulateView: target.simulateViewMode, @@ -342,7 +345,9 @@ export const EditorProvider: React.FC = ({ children }) => { : "simulation-resource" : target.simulateViewMode !== undefined ? "simulation-view" - : "mode", + : target.editViewMode !== undefined + ? "edit-view" + : "mode", }, ); @@ -360,6 +365,7 @@ export const EditorProvider: React.FC = ({ children }) => { const actions: Omit = { navigateTo, setGlobalMode: (mode) => navigateTo({ globalMode: mode }), + setEditViewMode: (view) => navigateTo({ editViewMode: view }), setEditionMode: (mode) => setState((prev) => ({ ...prev, @@ -562,6 +568,7 @@ export const EditorProvider: React.FC = ({ children }) => { const effectiveState: EditorState = { ...state, globalMode: navigation.state.mode, + editViewMode: navigation.state.editView, simulateViewMode: navigation.state.simulateView, simulateDrawer: navigationResourceToSimulateDrawer( navigation.state.simulateResource, diff --git a/libs/@hashintel/petrinaut/src/react/state/use-effective-global-mode.ts b/libs/@hashintel/petrinaut/src/react/state/use-effective-global-mode.ts deleted file mode 100644 index 5c21f1bc014..00000000000 --- a/libs/@hashintel/petrinaut/src/react/state/use-effective-global-mode.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { use } from "react"; - -import { EditorContext } from "./editor-context"; -import { UserSettingsContext } from "./user-settings-context"; - -import type { EditorGlobalMode } from "./editor-context"; - -/** - * The global mode the editor actually renders. The stored mode can say - * "notebook" while the experimental notebook flag is off (e.g. the flag was - * disabled while the view was active); every consumer must agree that this - * falls back to "edit" — deriving it in one consumer only would render the - * edit canvas while mutations are still refused with a notebook explanation. - */ -export const useEffectiveGlobalMode = (): EditorGlobalMode => { - const { globalMode } = use(EditorContext); - const { enableNotebookView } = use(UserSettingsContext); - - return globalMode === "notebook" && !enableNotebookView ? "edit" : globalMode; -}; diff --git a/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts b/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts index c9caf5f6344..f944517d199 100644 --- a/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts +++ b/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts @@ -1,8 +1,8 @@ import { use } from "react"; import { SimulationContext } from "../simulation/context"; +import { EditorContext } from "./editor-context"; import { SDCPNContext } from "./sdcpn-context"; -import { useEffectiveGlobalMode } from "./use-effective-global-mode"; /** * Why the editor currently disallows mutations, or `null` when mutations @@ -26,10 +26,7 @@ export type ReadOnlyReason = */ export const useReadOnlyReason = (): ReadOnlyReason | null => { const { readonly } = use(SDCPNContext); - // The effective mode, not the stored one — the stored mode can say - // "notebook" while the flag is off, in which case the edit canvas renders - // and mutations must be allowed. - const globalMode = useEffectiveGlobalMode(); + const { globalMode } = use(EditorContext); const { state: simulationState } = use(SimulationContext); if (readonly) { 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..a7a08c834ea 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-context.ts @@ -54,7 +54,6 @@ export type UserSettings = { highlightOnHover: boolean; partialSelection: boolean; enableNetComponents: boolean; - enableNotebookView: boolean; /** * Persisted preference controlling whether the product walkthrough opens * automatically the next time the app initializes. The live open state is @@ -130,7 +129,6 @@ export type UserSettingsActions = { setHighlightOnHover: (value: boolean) => void; setPartialSelection: (value: boolean) => void; setEnableNetComponents: (value: boolean) => void; - setEnableNotebookView: (value: boolean) => void; setShowWalkthroughOnInit: (value: boolean) => void; setWebGpuEnabled: (value: boolean) => void; setShowCompilationOutput: (value: boolean) => void; @@ -167,7 +165,6 @@ export const defaultUserSettings: UserSettings = { highlightOnHover: true, partialSelection: true, enableNetComponents: false, - enableNotebookView: false, showWalkthroughOnInit: true, webGpuEnabled: false, showCompilationOutput: false, @@ -203,7 +200,6 @@ export const defaultUserSettingsContextValue: UserSettingsContextValue = { setHighlightOnHover: () => {}, setPartialSelection: () => {}, setEnableNetComponents: () => {}, - setEnableNotebookView: () => {}, 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..f499cd8ac83 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 @@ -114,7 +114,7 @@ describe("UserSettingsProvider", () => { expect(screen.getByRole("button", { name: "probe: on" })).toBe(probe); }); - it("loads a blob written with the retired Ad-hoc scenarios key and drops the key on the next write", () => { + it("loads saved preferences and drops retired experiment keys on the next write", () => { // An in-memory store: some Node versions expose a global `localStorage` // whose methods are missing, so the test owns the storage it inspects. const entries = new Map([ @@ -122,6 +122,7 @@ describe("UserSettingsProvider", () => { storageKey, JSON.stringify({ enableAdHocScenarios: true, + enableNotebookView: false, showWalkthroughOnInit: false, }), ], @@ -146,6 +147,7 @@ describe("UserSettingsProvider", () => { unknown >; expect("enableAdHocScenarios" in persisted).toBe(false); + expect("enableNotebookView" in persisted).toBe(false); expect(persisted.showWalkthroughOnInit).toBe(false); expect(persisted.brunchDemoMode).toBe(true); } finally { 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..b40955082c7 100644 --- a/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/state/user-settings-provider.tsx @@ -43,6 +43,7 @@ type PersistedUserSettings = Partial & { * scenario form, so the key is dropped on the next write. */ enableAdHocScenarios?: boolean; + enableNotebookView?: boolean; }; const loadSettings = (): UserSettings => { @@ -56,6 +57,7 @@ const loadSettings = (): UserSettings => { useEntitiesTreeView: _useEntitiesTreeView, enableOptimizationSurface: _enableOptimizationSurface, enableAdHocScenarios: _enableAdHocScenarios, + enableNotebookView: _enableNotebookView, ...parsed } = JSON.parse(raw) as PersistedUserSettings; return { @@ -130,8 +132,6 @@ const OwnedUserSettingsProvider: React.FC = ({ setState((prev) => ({ ...prev, partialSelection: value })), setEnableNetComponents: (value: boolean) => setState((prev) => ({ ...prev, enableNetComponents: value })), - setEnableNotebookView: (value: boolean) => - setState((prev) => ({ ...prev, enableNotebookView: value })), setShowWalkthroughOnInit: (value: boolean) => setState((prev) => ({ ...prev, showWalkthroughOnInit: value })), setWebGpuEnabled: (value: boolean) => diff --git a/libs/@hashintel/petrinaut/src/ui/components/sub-view/types.ts b/libs/@hashintel/petrinaut/src/ui/components/sub-view/types.ts index 8de203cc3dd..4d97a19ecfa 100644 --- a/libs/@hashintel/petrinaut/src/ui/components/sub-view/types.ts +++ b/libs/@hashintel/petrinaut/src/ui/components/sub-view/types.ts @@ -77,6 +77,7 @@ export interface SubView { * Defaults to false. */ alwaysShowHeaderAction?: boolean; + canMaximize?: boolean; /** * Configuration for making the subview resizable when expanded. * Only affects vertical layout. When set, the section can be resized by dragging its bottom edge. diff --git a/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.test.tsx b/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.test.tsx new file mode 100644 index 00000000000..f81a104e579 --- /dev/null +++ b/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.test.tsx @@ -0,0 +1,512 @@ +/** @vitest-environment jsdom */ +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; +import { useState } from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + defaultPetrinautNavigationState, + PetrinautNavigationProvider, +} from "../../../../react/navigation"; +import { + defaultUserSettingsContextValue, + UserSettingsContext, +} from "../../../../react/state/user-settings-context"; +import { PetrinautPresentationProvider } from "../../../views/shared/presentation-context"; +import { VerticalSubViewsContainer } from "./vertical-sub-views-container"; + +import type { PetrinautNavigationController } from "../../../../react/navigation"; +import type { SubView } from "../types"; +import type { ComponentProps } from "react"; + +vi.mock("@hashintel/ds-components", async (importOriginal) => { + const components = + await importOriginal(); + return { + ...components, + Button: (props: ComponentProps) => ( + + ), + }; +}); + +const updateSubViewSection = vi.fn(); +const originalAnimate = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + "animate", +); + +beforeEach(() => { + vi.stubGlobal("matchMedia", () => ({ + matches: false, + addEventListener: () => {}, + removeEventListener: () => {}, + })); + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); + vi.stubGlobal( + "IntersectionObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); +}); + +afterEach(() => { + cleanup(); + if (originalAnimate) + Object.defineProperty(HTMLElement.prototype, "animate", originalAnimate); + else Reflect.deleteProperty(HTMLElement.prototype, "animate"); + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); +}); + +const Properties = () => { + const [name, setName] = useState("Transition"); + return ( + setName(event.target.value)} + /> + ); +}; + +const Code = () => { + const [code, setCode] = useState("return true;"); + return ( + <> + +