From b771259bd6fd9ac7aa0a25f6ad4e274ba0876411 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 15 Sep 2026 02:46:34 +0200 Subject: [PATCH 1/3] Tighten and label place-property checkboxes --- .changeset/property-checkbox-spacing.md | 5 +++++ .../place-properties/subviews/main.tsx | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 .changeset/property-checkbox-spacing.md diff --git a/.changeset/property-checkbox-spacing.md b/.changeset/property-checkbox-spacing.md new file mode 100644 index 00000000000..7052aef289f --- /dev/null +++ b/.changeset/property-checkbox-spacing.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Give place-property checkboxes consistent sizing, spacing, and accessible names. 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 d86a69536df..6a978c2332c 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 @@ -38,6 +38,10 @@ const jumpButtonContainerStyle = css({ textAlign: "right", }); +const sectionCheckboxStyle = css({ + marginRight: "1", +}); + const hintTextStyle = css({ fontSize: "[12px]", color: "neutral.s95", @@ -397,6 +401,9 @@ const PlaceMainContent: React.FC = () => { tooltip="Exposes this place as an arc endpoint when its subnet is instantiated as a component." renderHeaderLeading={() => ( { @@ -420,6 +427,8 @@ const PlaceMainContent: React.FC = () => { renderHeaderLeading={() => ( { @@ -476,6 +485,8 @@ const PlaceMainContent: React.FC = () => { renderHeaderLeading={() => ( { From 10a00cb67b33335f81feb1c2f198829d317e49c1 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Tue, 15 Sep 2026 23:07:16 +0200 Subject: [PATCH 2/3] Polish properties panel controls and navigation --- .changeset/property-checkbox-spacing.md | 2 +- .../petrinaut/docs/drawing-a-net.md | 10 + .../petrinaut/docs/petri-net-extensions.md | 4 +- .../petrinaut/src/ui/components/section.tsx | 2 +- .../vertical/vertical-sub-views-container.tsx | 55 +- .../subviews/filterable-list-sub-view.tsx | 12 +- .../PropertiesPanel/arc-properties/main.tsx | 6 +- .../subviews/main.tsx | 8 +- .../place-properties/subviews/main.test.tsx | 183 +++++ .../place-properties/subviews/main.tsx | 675 ++++++++---------- .../subviews/place-initial-state/subview.tsx | 1 + .../subviews/place-visualizer/subview.tsx | 3 +- .../transition-properties/subviews/main.tsx | 8 +- 13 files changed, 527 insertions(+), 442 deletions(-) create mode 100644 libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/place-properties/subviews/main.test.tsx diff --git a/.changeset/property-checkbox-spacing.md b/.changeset/property-checkbox-spacing.md index 7052aef289f..8e07cdf6da0 100644 --- a/.changeset/property-checkbox-spacing.md +++ b/.changeset/property-checkbox-spacing.md @@ -2,4 +2,4 @@ "@hashintel/petrinaut": patch --- -Give place-property checkboxes consistent sizing, spacing, and accessible names. +Polish properties panels with clickable checkbox labels, compact navigation buttons, consistent fields, and clearer section controls and dividers. diff --git a/libs/@hashintel/petrinaut/docs/drawing-a-net.md b/libs/@hashintel/petrinaut/docs/drawing-a-net.md index a5131580fbe..67b2f5615cd 100644 --- a/libs/@hashintel/petrinaut/docs/drawing-a-net.md +++ b/libs/@hashintel/petrinaut/docs/drawing-a-net.md @@ -21,6 +21,16 @@ into it, and the rest comes back for as long as you stay on it. full-editor +## Properties panel + +Select an entity to inspect its properties. Places use the same Name and Description fields as transitions and types. + +Place settings include an **Options** section for **Component port**, **Token capacity**, and **Default starting place**. Click a checkbox or its label to toggle the setting. Enabling Token capacity shows the **Maximum tokens** field. Point at the info icons for an explanation of each option. + +Click **View type** or **View equation** to open the assigned definition. These navigation actions are available when the net is read-only. + +The **State** and **Visualizer** section headers have visible expand arrows. Click a header to expand its content and drag the divider to give it more room. The Visualizer switch remains available while its section is collapsed. + ## Top bar Spans the full editor width and has three sections. diff --git a/libs/@hashintel/petrinaut/docs/petri-net-extensions.md b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md index 148d3c8d7dd..835cf00373b 100644 --- a/libs/@hashintel/petrinaut/docs/petri-net-extensions.md +++ b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md @@ -73,7 +73,7 @@ Differential equations define how token data evolves continuously over time. The 1. Create a differential equation in the **Differential Equations** group (left sidebar). 2. Give it a name and associate it with a **type** (the equation applies to tokens of that type). -3. Select a place, enable **Dynamics**, and choose an equation that matches the type assigned to the place. +3. Select a place, enable **Dynamics**, and choose an equation that matches the type assigned to the place. Click **View equation** below the dropdown to open its definition. **Function body:** @@ -93,7 +93,7 @@ The code is a plain function body ending in `return`, with `tokens` (the current A visualizer renders a custom view of a place's tokens during simulation. It is a React component that returns JSX (SVG is recommended). -**To enable:** select a place, then toggle **Visualizer** in its properties. A code editor opens. +**To enable:** select a place, then turn on the **Visualizer** switch in its properties. Click the **Visualizer** header to expand its code editor. ```tsx export default Visualization(({ tokens, parameters }) => { diff --git a/libs/@hashintel/petrinaut/src/ui/components/section.tsx b/libs/@hashintel/petrinaut/src/ui/components/section.tsx index 139f03304f0..b25d32b7b3a 100644 --- a/libs/@hashintel/petrinaut/src/ui/components/section.tsx +++ b/libs/@hashintel/petrinaut/src/ui/components/section.tsx @@ -19,7 +19,7 @@ const sectionListStyle = css({ "& > *:not(:last-child)": { borderBottomWidth: "[1px]", borderBottomStyle: "solid", - borderBottomColor: "neutral.a20", + borderBottomColor: "neutral.a40", }, }); diff --git a/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.tsx b/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.tsx index 6d7495a9fcd..0c745c9d8b8 100644 --- a/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.tsx +++ b/libs/@hashintel/petrinaut/src/ui/components/sub-view/vertical/vertical-sub-views-container.tsx @@ -192,7 +192,7 @@ const scrollShadowStyle = cva({ const resizeHandleStyle = css({ borderTopWidth: "thin", - borderTopColor: "neutral.a20", + borderTopColor: "neutral.a40", cursor: "ns-resize", backgroundColor: "[transparent]", transition: "[background-color 0.15s ease]", @@ -219,7 +219,7 @@ const headerRowStyle = cva({ alignItems: "center", borderBottomWidth: "thin", - borderBottomColor: "neutral.a20", + borderBottomColor: "neutral.a40", }, variants: { isCollapsed: { @@ -240,7 +240,7 @@ const mainHeaderRowStyle = css({ alignItems: "center", borderBottomWidth: "thin", - borderBottomColor: "neutral.a20", + borderBottomColor: "neutral.a40", }); const headerActionVisibleStyle = css({ @@ -273,12 +273,8 @@ const sectionToggleStyle = css({ minWidth: "[0]", overflow: "hidden", - /* Reveal the chevron icon on toggle section hover */ "& [data-toggle-icon]": { width: "3.5", - opacity: "[0]", - }, - "&:hover [data-toggle-icon]": { opacity: "[1]", }, }); @@ -534,27 +530,30 @@ const SubViewHeader: React.FC = ({ )} - {isExpanded && (renderHeaderAction || onExpand) && ( -
- {onExpand && ( -
- )} + {(isExpanded || alwaysShowHeaderAction) && + (renderHeaderAction || onExpand) && ( +
+ {isExpanded && onExpand && ( +
+ )} ); diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.tsx index 4328f705489..509e32a2fbb 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.tsx @@ -45,9 +45,9 @@ const listItemRowStyle = cva({ minHeight: "8", p: "1", borderRadius: "lg", - fontSize: "sm", - fontWeight: "medium", - color: "neutral.s115", + fontSize: "[13px]", + fontWeight: "normal", + color: "neutral.fg.body", backgroundColor: "[transparent]", cursor: "pointer", transition: "[background-color 100ms ease-out, opacity 150ms ease-out]", @@ -103,10 +103,10 @@ const listItemContentStyle = css({ const listItemNameStyle = css({ flex: "[1]", minWidth: "[0]", - fontSize: "sm", - fontWeight: "medium", + fontSize: "[13px]", + fontWeight: "normal", lineHeight: "snug", - color: "neutral.s115", + color: "neutral.fg.body", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx index b4763d48ddd..674c20f6746 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/arc-properties/main.tsx @@ -160,8 +160,8 @@ const DeleteArcAction: React.FC = () => { return ( + )} + )} -
- - updatePlace({ - placeId: place.id, - update: { description }, - }) - } - disabled={isReadOnly} - tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} - /> -
+ {extensions.colors && extensions.dynamics && ( +
{ + const dynamicsTooltip = isReadOnly + ? UI_MESSAGES.READ_ONLY_MODE + : place.colorId === null + ? UI_MESSAGES.DYNAMICS_REQUIRES_TYPE + : availableDiffEqs.length === 0 + ? "Create a differential equation for this type first" + : undefined; - {extensions.colors && ( -
- + return ( - { - if (differentialEquationId) { - updatePlace({ - placeId: place.id, - update: { differentialEquationId }, + + {place.differentialEquationId && ( +
+ + {codeNavigation.enabled && ( - {codeNavigation.enabled && ( - - )} -
- )} - - ) - )} -
- )} -
( - { - updatePlace({ - placeId: place.id, - update: { isPort: checked === true }, - }); - }} - /> + )} + + )} + + ) )} - > -
- {place.isPort - ? "Transitions in the parent net can connect arcs to this subnet place through a component instance." - : "Enable this for subnet boundary places that should be available as component instance arc endpoints."} -
-
( - +
+ + updatePlace({ placeId: place.id, update: { isPort: checked } }) + } + /> + +
+
+ + updatePlace({ + placeId: place.id, + update: { capacity: checked ? 1 : null }, + }) + } + /> + +
+ {place.capacity !== undefined && place.capacity !== null && ( + + { - updatePlace({ - placeId: place.id, - // Starts at 1; a scenario marking above the cap fails the - // build with a message naming the place, so a too-low - // default is loud rather than silent. - update: { capacity: checked === true ? 1 : null }, - }); + min={0} + max={4294967294} + value={place.capacity} + onChange={(nextCapacity) => { + if ( + nextCapacity !== null && + nextCapacity >= 0 && + nextCapacity <= 4294967294 + ) { + updatePlace({ + placeId: place.id, + update: { capacity: Math.floor(nextCapacity) }, + }); + } }} - /> - )} - > - {place.capacity === undefined || place.capacity === null ? ( -
- This place can hold any number of tokens. Enable to cap it and - block the transitions that feed it once it is full. -
- ) : ( - <> - { - if ( - nextCapacity !== null && - nextCapacity >= 0 && - nextCapacity <= 4294967294 - ) { - updatePlace({ - placeId: place.id, - update: { capacity: Math.floor(nextCapacity) }, - }); - } - }} - disabled={isReadOnly} - /> -
- A transition whose firing would take this place above{" "} - {place.capacity} {place.capacity === 1 ? "token" : "tokens"}{" "} - cannot fire, the same way a transition without enough input - tokens cannot fire. -
- - )} -
-
( - { - updatePlace({ - placeId: place.id, - update: { showAsInitialState: checked === true }, - }); - }} /> - )} - > -
- {place.showAsInitialState - ? "This place should have an initial marking defined to run the net, and will be pre-selected in new scenarios." - : "Enable if this place should have an initial marking defined to run the net. It will be pre-selected in new scenarios."} -
-
- - + + )} +
+ + updatePlace({ + placeId: place.id, + update: { showAsInitialState: checked }, + }) + } + /> + +
+
+ ); }; @@ -515,14 +404,14 @@ const DeletePlaceAction: React.FC = () => { return ( - {codeNavigation.enabled && ( - - )} )} @@ -330,7 +333,7 @@ const PlaceMainContent: React.FC = () => {
{ />
-
- - updatePlace({ - placeId: place.id, - update: { capacity: checked ? 1 : null }, - }) - } - /> - -
- {place.capacity !== undefined && place.capacity !== null && ( - - +
+ { - if ( - nextCapacity !== null && - nextCapacity >= 0 && - nextCapacity <= 4294967294 - ) { - updatePlace({ - placeId: place.id, - update: { capacity: Math.floor(nextCapacity) }, - }); - } - }} + label="Token capacity" + value={capacityEnabled} disabled={isReadOnly} + onChange={(checked) => + updatePlace({ + placeId: place.id, + update: { capacity: checked ? 1 : null }, + }) + } /> - - )} + +
+ + + { + if ( + nextCapacity !== null && + nextCapacity >= 0 && + nextCapacity <= 4294967294 + ) { + updatePlace({ + placeId: place.id, + update: { capacity: Math.floor(nextCapacity) }, + }); + } + }} + disabled={isReadOnly} + /> + + +
{}, - existingNets: [], - loadPetriNet: () => {}, - petriNetId: "story-net", - petriNetDefinition: { - places: PLACES, - transitions: [TRANSITION], - types: TYPES, - differentialEquations: DIFF_EQS, - parameters: [PARAMETER], - }, - readonly: false, - extensions: DEFAULT_PETRINAUT_EXTENSIONS, - setTitle: () => {}, - title: "Story Net", - getItemType: () => null, +const emptyPlace: Place = { + id: "place-empty", + name: "NewPlace", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 0, + y: 0, }; -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- +const emptyTransition: Transition = { + id: "transition-empty", + name: "NewTransition", + inputArcs: [], + outputArcs: [], + lambdaType: "predicate", + lambdaCode: "", + transitionKernelCode: "", + x: 0, + y: 0, +}; -/** Deep-clone story state so local Storybook updates mimic app mutations. */ -function clone(value: T): T { - return JSON.parse(JSON.stringify(value)) as T; -} +const PropertiesPanelStory = ({ selection }: { selection: SelectionItem }) => { + const [instance] = useState(() => + createPetrinaut({ + document: createJsonDocHandle({ + id: "properties-panel-story", + initial: { + places: [...places, emptyPlace], + transitions: [transition, emptyTransition], + types, + differentialEquations, + parameters: [parameter], + }, + }), + }), + ); + const [navigationState, setNavigationState] = + useState(() => ({ + ...defaultPetrinautNavigationState, + selection: [selection], + })); -function applyStoryUpdate( - setValue: Dispatch>, - update: Partial, -): void { - setValue((prev) => ({ ...clone(prev), ...clone(update) })); -} + useEffect(() => () => instance.dispose(), [instance]); -/** Wraps panel content with the required contexts, filling the viewport. */ -const PanelFrame = ({ children }: { children: ReactNode }) => ( - - -
+ return ( + {}, + loadPetriNet: () => {}, + }} + navigation={{ state: navigationState, onNavigate: setNavigationState }} + > +
- {children} +
+ +
-
-
-
-); - -// --------------------------------------------------------------------------- -// Interactive story components (manage local state) -// --------------------------------------------------------------------------- - -const PlacePanelStory = () => { - const [place, setPlace] = useState(PLACES[0]!); - const updatePlace: PetrinautMutations["updatePlace"] = (input) => { - applyStoryUpdate(setPlace, input.update); - }; - return ( - - - - ); -}; - -const PlaceEmptyPanelStory = () => { - const [place, setPlace] = useState({ - id: "place-empty", - name: "NewPlace", - colorId: null, - dynamicsEnabled: false, - differentialEquationId: null, - x: 0, - y: 0, - }); - const updatePlace: PetrinautMutations["updatePlace"] = (input) => { - applyStoryUpdate(setPlace, input.update); - }; - return ( - - - - ); -}; - -const TransitionPanelStory = () => { - const [transition, setTransition] = useState(TRANSITION); - const endpointFromInput = (input: { - placeId?: string; - endpoint?: Parameters[1]; - }) => input.endpoint ?? placeArcEndpoint(input.placeId ?? ""); - const updateTransition: PetrinautMutations["updateTransition"] = (input) => { - applyStoryUpdate(setTransition, input.update); - }; - const updateArcWeight: PetrinautMutations["updateArcWeight"] = (input) => { - setTransition((prev) => { - const next = clone(prev); - const arcs = - input.arcDirection === "input" ? next.inputArcs : next.outputArcs; - const endpoint = endpointFromInput(input); - const arc = arcs.find((item) => arcMatchesEndpoint(item, endpoint)); - if (arc) { - arc.weight = input.weight; - } - return next; - }); - }; - const updateArcPlace: PetrinautMutations["updateArcPlace"] = (input) => { - setTransition((prev) => { - const next = clone(prev); - const arcs = - input.arcDirection === "input" ? next.inputArcs : next.outputArcs; - const oldEndpoint = - input.oldEndpoint ?? placeArcEndpoint(input.oldPlaceId ?? ""); - const newEndpoint = - input.newEndpoint ?? placeArcEndpoint(input.newPlaceId ?? ""); - const arc = arcs.find((item) => arcMatchesEndpoint(item, oldEndpoint)); - if (arc) { - delete arc.placeId; - delete arc.endpoint; - Object.assign( - arc, - newEndpoint.kind === "place" - ? { placeId: newEndpoint.placeId } - : { endpoint: newEndpoint }, - ); - } - return next; - }); - }; - const removeArc: PetrinautMutations["removeArc"] = (input) => { - setTransition((prev) => { - const next = clone(prev); - const arcs = - input.arcDirection === "input" ? next.inputArcs : next.outputArcs; - const endpoint = endpointFromInput(input); - const index = arcs.findIndex((arc) => arcMatchesEndpoint(arc, endpoint)); - if (index !== -1) { - arcs.splice(index, 1); - } - return next; - }); - }; - return ( - - - - ); -}; - -const TransitionEmptyPanelStory = () => { - const [transition, setTransition] = useState({ - id: "transition-empty", - name: "NewTransition", - inputArcs: [], - outputArcs: [], - lambdaType: "predicate", - lambdaCode: "", - transitionKernelCode: "", - x: 0, - y: 0, - }); - const updateTransition: PetrinautMutations["updateTransition"] = (input) => { - applyStoryUpdate(setTransition, input.update); - }; - return ( - - {}} - updateArcPlace={() => {}} - removeArc={() => {}} - /> - - ); -}; - -const TypePanelStory = () => { - const [type, setType] = useState(TYPES[0]!); - const updateType: PetrinautMutations["updateType"] = (input) => { - applyStoryUpdate(setType, input.update); - }; - const addTypeElement: PetrinautMutations["addTypeElement"] = (input) => { - setType((prev) => { - const next = clone(prev); - next.elements.push(clone(input.element)); - return next; - }); - }; - const updateTypeElement: PetrinautMutations["updateTypeElement"] = ( - input, - ) => { - setType((prev) => { - const next = clone(prev); - const element = next.elements.find( - (item) => item.elementId === input.elementId, - ); - if (element) { - Object.assign(element, clone(input.update)); - } - return next; - }); - }; - const removeTypeElement: PetrinautMutations["removeTypeElement"] = ( - input, - ) => { - setType((prev) => ({ - ...clone(prev), - elements: prev.elements.filter( - (element) => element.elementId !== input.elementId, - ), - })); - }; - const moveTypeElement: PetrinautMutations["moveTypeElement"] = (input) => { - setType((prev) => { - const next = clone(prev); - const fromIndex = next.elements.findIndex( - (element) => element.elementId === input.elementId, - ); - if (fromIndex !== -1) { - const [element] = next.elements.splice(fromIndex, 1); - if (element) { - next.elements.splice(input.toIndex, 0, element); - } - } - return next; - }); - }; - return ( - - - + + ); }; -const ParameterPanelStory = () => { - const [parameter, setParameter] = useState(PARAMETER); - const updateParameter: PetrinautMutations["updateParameter"] = (input) => { - applyStoryUpdate(setParameter, input.update); - }; - return ( - - - - ); -}; - -const DifferentialEquationPanelStory = () => { - const [diffEq, setDiffEq] = useState(DIFF_EQS[0]!); - const updateDiffEq: PetrinautMutations["updateDifferentialEquation"] = ( - input, - ) => { - applyStoryUpdate(setDiffEq, input.update); - }; - return ( - - - - ); -}; - -// --------------------------------------------------------------------------- -// Storybook -// --------------------------------------------------------------------------- - const meta = { title: "Panels / Properties Panel", + component: PropertiesPanelStory, parameters: { layout: "fullscreen", }, -} satisfies Meta; + render: (args) => , +} satisfies Meta; export default meta; @@ -462,35 +220,52 @@ type Story = StoryObj; export const PlaceWithType: Story = { name: "Place (with type & dynamics)", - render: () => , + args: { selection: { type: "place", id: "place-1" } }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect( + await canvas.findByRole("textbox", { name: "Name" }), + ).toHaveValue("PlantASupply"); + for (const label of [ + "Component port", + "Token capacity", + "Default starting place", + ]) { + const checkbox = canvas.getByRole("checkbox", { name: label }); + await userEvent.click(canvas.getByText(label, { exact: true })); + await expect(checkbox).toBeChecked(); + await userEvent.click(canvas.getByText(label, { exact: true })); + await expect(checkbox).not.toBeChecked(); + } + }, }; export const PlaceEmpty: Story = { name: "Place (no type)", - render: () => , + args: { selection: { type: "place", id: "place-empty" } }, }; export const TransitionWithArcs: Story = { name: "Transition (with arcs)", - render: () => , + args: { selection: { type: "transition", id: "transition-1" } }, }; export const TransitionEmpty: Story = { name: "Transition (empty)", - render: () => , + args: { selection: { type: "transition", id: "transition-empty" } }, }; export const Type: Story = { name: "Type", - render: () => , + args: { selection: { type: "type", id: "type-1" } }, }; export const ParameterPanel: Story = { name: "Parameter", - render: () => , + args: { selection: { type: "parameter", id: "param-1" } }, }; export const DifferentialEquationPanel: Story = { name: "Differential Equation", - render: () => , + args: { selection: { type: "differentialEquation", id: "eq-1" } }, };