Skip to content

FE-504: Add copy/paste and select all to petrinaut editor#8533

Open
kube wants to merge 24 commits intomainfrom
cf/petrinaut-copy-paste
Open

FE-504: Add copy/paste and select all to petrinaut editor#8533
kube wants to merge 24 commits intomainfrom
cf/petrinaut-copy-paste

Conversation

@kube
Copy link
Collaborator

@kube kube commented Mar 12, 2026

🌟 What is the purpose of this PR?

Add clipboard support and selection shortcuts to the petrinaut editor, enabling users to copy, paste, and select all items in the SDCPN canvas.

🔗 Related links

🔍 What does this change?

  • Copy (Cmd/Ctrl+C): Serializes selected items (places, transitions, token types, differential equations, parameters) to clipboard as a versioned JSON format (petrinaut-sdcpn v1). Arcs between selected nodes are preserved within their transitions; arcs to unselected nodes are stripped.
  • Paste (Cmd/Ctrl+V): Deserializes clipboard data, generates new UUIDs for all items, deduplicates names with numeric suffixes, remaps internal references (arc→place, place→colorId/differentialEquationId, equation→colorId), and offsets pasted nodes by 50px. Newly pasted items are automatically selected.
  • Select All (Cmd/Ctrl+A): Selects all canvas nodes (places and transitions).
  • Escape to deselect: Clears selection in addition to switching to cursor mode.
  • Clipboard format: Versioned payload validated with zod, includes source document ID for future cross-document reconciliation.
  • zod added as a dependency for clipboard payload validation.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

🐾 Next steps

  • Cross-document paste with dependency reconciliation (copy from one document, paste into another — may need to bring referenced types/equations along)
  • Cut (Cmd/Ctrl+X) support

🛡 What tests cover this?

  • 77 unit tests across 3 test files:
    • deduplicate-name.test.ts — name suffix logic (12 tests)
    • serialize.test.ts — serialization, arc stripping, payload parsing with zod validation (32 tests)
    • paste.test.ts — ID generation, name deduplication, position offset, arc remapping, reference remapping, data preservation (33 tests)

❓ How to test this?

  1. Checkout the branch
  2. Open the petrinaut editor with a net containing places, transitions, types, equations, and parameters
  3. Select items and press Cmd+C, then Cmd+V — confirm duplicates appear with new names and offset positions
  4. Press Cmd+A — confirm all canvas nodes are selected
  5. Press Escape — confirm selection is cleared
  6. Copy items from one document and paste into another — confirm they paste as independent copies

@vercel
Copy link

vercel bot commented Mar 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Error Error Mar 12, 2026 10:56pm
petrinaut Ready Ready Preview, Comment Mar 12, 2026 10:56pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign Ignored Ignored Preview Mar 12, 2026 10:56pm
hashdotdesign-tokens Ignored Ignored Preview Mar 12, 2026 10:56pm

@github-actions github-actions bot added area/deps Relates to third-party dependencies (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Mar 12, 2026
Copy link
Collaborator Author

kube commented Mar 12, 2026

@kube kube changed the title Make TopBar title input fill available space FE-504: Add copy/paste and select all to petrinaut editor Mar 12, 2026
@github-actions github-actions bot added the area/infra Relates to version control, CI, CD or IaC (area) label Mar 12, 2026
kube and others added 19 commits March 12, 2026 23:38
Extract common filterable list pattern into createFilterableListSubView factory.
Refactor nodes-list, types-list, parameters-list, and differential-equations-list
to use the shared component, reducing duplication of selection handling, row styles,
and empty state rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…is menu

- Update list item styles: 32px height, 8px border radius, 14px font,
  semantic color tokens, neutral hover/selected backgrounds
- Replace inline delete button with horizontal ellipsis (TbDots) that
  opens a Menu with a destructive "Delete" action
- Ellipsis button fades in on row hover with subtle icon slide animation,
  stays visible while menu is open via data-state selector
- Add text overflow ellipsis to all list item names
- Update nodes-list font size and colors to match design tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tons

- Add getMenuItems config param so the container owns Menu rendering
- Extract RowMenu component that skips rendering when items array is empty
- Move menu item definitions from renderItem to getMenuItems in all subviews
- Replace TbFilter with LuListFilter, add LuArrowDownWideNarrow (sort) and
  LuSearch (search) icon buttons in the header (no-ops for now)
- Place filter/sort/search buttons before subview-specific actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hen menu open

- Use stopPropagation on ellipsis button instead of DOM query guard
- Show hover background on row when its menu is open via :has selector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Scope chevron hover to toggle section only, not full header row
- Show header actions and info tooltip only on hover/focus-within with opacity animation
- Add outlined variant to InfoIconTooltip, used in subview headers
- Move item icons to FilterableListItem.icon prop with consistent rendering
- Wrap list items in content/name containers in filterable-list-sub-view
- Remove redundant wrapper styles from individual subview lists
- Add alwaysShowHeaderAction option to SubView, used by visualizer
- Align row menu to bottom-end

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add optional `icon` prop to SubView type and update VerticalSubViewsContainer
to render distinct main vs collapsible header styles matching the Figma design.
Main headers now show an outline icon + subtle text with a bottom border.
All PropertiesPanel main SubViews (Place, Transition, Arc, Parameter, Type,
Differential Equation) now include Lucide outline icons and use
alwaysShowHeaderAction where applicable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create constants/entity-icons.tsx as the single source of truth for all
Petri net entity icons (outline and filled variants). Update
FilterableListSubView to control icon size and default color centrally,
with an iconColor override used by TokenTypes for per-type coloring.
All list subviews now show their entity icon consistently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… subviews

Internalize listItemNameStyle within FilterableListSubView so renderItem
returns plain text/nodes wrapped automatically. Simplify all list subview
renderItem callbacks to return strings instead of styled spans. Reorder
LEFT_SIDEBAR_SUBVIEWS to show Nodes first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change SubView.icon from ReactNode to ComponentType<{ size: number }> so
the container controls the icon size (HEADER_ICON_SIZE = 16). All SubView
consumers now pass icon components directly instead of rendered elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e entity icons

Clicking in the list container but not on an item now calls clearSelection
to deselect all. Also update Parameter/TokenType/DifferentialEquation icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase default panel content horizontal padding to 4 and use negative
margin on FilterableListSubView to reduce its effective padding to 3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…terable list and menu

- Add Arrow Up/Down keyboard navigation with focused item tracking
- Support Shift+Click and Shift+Arrow range selection via selectRange helper
- Ctrl/Cmd+Click toggles multi-selection with anchor tracking
- Escape clears selection and resets focus state
- Clamp focus/anchor indices when item list shrinks
- Auto-scroll focused items into view
- Add ARIA listbox/option roles for accessibility
- Suppress default browser focus outline on list container
- Add _highlighted styles to Menu items and submenu triggers for keyboard focus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert headerRowStyle to cva with isCollapsed variant that sets
borderBottomColor to transparent, avoiding visual double-borders
between collapsed sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kube and others added 5 commits March 12, 2026 23:38
Replace raw [44px] with token value for consistent sizing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clear focusedIndex and anchorIndex on blur when focus moves outside
the list container, so stale keyboard state doesn't persist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fixed minWidth with flex: 1 so the title input expands
to use all remaining space in the left section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement clipboard support with a versioned JSON format (petrinaut-sdcpn v1)
validated by zod. Copy serializes selected items (places, transitions, token
types, differential equations, parameters) with arc preservation. Paste
duplicates items with new UUIDs, deduplicates names, remaps internal references,
and offsets node positions. Cmd+A selects all canvas nodes, Escape clears
selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kube kube changed the base branch from cf/fe-512-create-filterablelistsubview-component to graphite-base/8533 March 12, 2026 22:38
@kube kube force-pushed the cf/petrinaut-copy-paste branch from 97b4e36 to 7889b18 Compare March 12, 2026 22:38
@kube kube force-pushed the graphite-base/8533 branch from 39823b5 to 44fe283 Compare March 12, 2026 22:38
@kube kube changed the base branch from graphite-base/8533 to main March 12, 2026 22:38
@kube kube marked this pull request as ready for review March 13, 2026 00:39
@cursor
Copy link

cursor bot commented Mar 13, 2026

PR Summary

Medium Risk
Adds system clipboard integration and mutating paste logic plus broad keyboard shortcut handling; mistakes could lead to incorrect graph duplication, selection behavior, or unexpected edits in read-only/simulation states.

Overview
Adds clipboard copy/paste support to the petrinaut editor: selected places/transitions/types/differential equations/parameters are serialized to a versioned petrinaut-sdcpn JSON payload and validated on paste via zod, then pasted by generating new IDs, deduplicating names, remapping internal references (arcs and IDs), and offsetting node positions.

Extends editor keyboard handling with Cmd/Ctrl+C, Cmd/Ctrl+V (auto-select newly pasted items), Cmd/Ctrl+A (select all nodes), and Escape clears selection.

Refactors left sidebar lists to a shared createFilterableListSubView with improved keyboard/multiselect behavior and per-row overflow menus (e.g., delete), and updates subview headers to support icons and always-visible header actions; includes associated UI style tweaks and a changeset bumping @hashintel/petrinaut minor.

Written by Cursor Bugbot for commit 7889b18. This will update automatically on new commits. Configure here.

@augmentcode
Copy link

augmentcode bot commented Mar 13, 2026

🤖 Augment PR Summary

Summary: Adds clipboard + selection keyboard shortcuts to the Petrinaut SDCPN editor, enabling users to copy/paste selections and quickly select/deselect canvas items.

Changes:

  • Introduces a versioned clipboard payload format (petrinaut-sdcpn v1) with Zod validation.
  • Adds selection serialization (including arc-stripping rules) and parsing helpers.
  • Implements paste logic that generates new UUIDs, remaps internal references, deduplicates names/variable names, and offsets pasted nodes.
  • Wires up Cmd/Ctrl+C, Cmd/Ctrl+V, Cmd/Ctrl+A, and Escape-to-deselect in the editor keyboard shortcut handler.
  • Adds extensive unit test coverage for deduplication, serialization/parsing, and paste behaviors.
  • Refactors left-sidebar lists to a shared “filterable list” subview and centralizes entity icon definitions; updates subview headers/tooltips styling.
  • Adds a changeset and introduces zod as a dependency.

Technical Notes: Clipboard payload is explicitly versioned, and paste performs ID remapping and reference preservation/remapping for types, equations, parameters, places, transitions, and arcs.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

export async function pasteFromClipboard(
mutatePetriNetDefinition: (mutateFn: (sdcpn: SDCPN) => void) => void,
): Promise<Array<{ type: string; id: string }> | null> {
const text = await navigator.clipboard.readText();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.clipboard.readText() / writeText() can throw (permissions, non-secure context, unsupported API), and right now that would surface as an unhandled rejection from the keyboard shortcut handler. Consider catching clipboard errors (and possibly checking navigator.clipboard existence) and returning null/no-op so paste/copy fails gracefully.

Severity: medium

Other Locations
  • libs/@hashintel/petrinaut/src/clipboard/clipboard.ts:22
  • libs/@hashintel/petrinaut/src/views/Editor/components/BottomBar/use-keyboard-shortcuts.ts:77

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

</IconButton>
{renderExtraAction?.()}
</>
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter/sort/search buttons rendered without click handlers

Low Severity

FilterHeaderAction renders three IconButton components (filter, sort, search) with no onClick handlers and no disabled prop. These buttons appear on hover for every list subview (nodes, types, parameters, equations) but are completely non-functional — clicking them does nothing. This is confusing for users who will see interactive-looking buttons that produce no result. If these are scaffolding for future functionality, they need to either be disabled or removed until implemented.

Fix in Cursor Fix in Web

),
);
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clipboard async operations lack error handling

Medium Severity

Both copySelectionToClipboard and pasteFromClipboard are fired with void and no .catch() handler. The clipboard API (navigator.clipboard.readText/writeText) commonly throws when the document isn't focused or clipboard permission is denied. These unhandled rejections will surface as uncaught promise errors and potentially trigger error-tracking noise (e.g., Sentry).

Fix in Cursor Fix in Web

disabled: isReadOnly,
onClick: () => removeType(type.id),
},
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooks called inside callback violate Rules of Hooks

Medium Severity

The getMenuItems callbacks in types-list.tsx, differential-equations-list.tsx, and parameters-list.tsx call hooks (use(SDCPNContext), use(EditorContext), useIsReadOnly()) inside a plain callback function rather than at the top level of a React component. Although these execute during RowMenu's render and happen to work, this pattern violates React's Rules of Hooks and will be flagged by react-hooks/rules-of-hooks. It's fragile and confusing for future maintainers.

Additional Locations (2)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant