zudo-panel-designer docs
GitHub repository

Type to search...

to open search from anywhere

Clipboard

Copy/Cut/Duplicate/Select All, the versioned OS-clipboard envelope, and paste-only ⌘V semantics.

use-clipboard.ts wires up copy, cut, duplicate, and select-all as commands in the command registry, plus a self-contained paste path that owns the browser's native paste event end to end.

Copy, cut, duplicate, select all

ShortcutActionEffect
⌘/Ctrl+CCopySnapshots the current selection to the internal clipboard and best-effort writes it to the OS clipboard.
⌘/Ctrl+XCutCopy, then delete the selection — as one undo entry.
⌘/Ctrl+DDuplicateClones the selection with fresh ids and a small offset, selects the clones — one undo entry.
⌘/Ctrl+ASelect AllSelects every non-pattern layer.

Copy, Cut, and Duplicate act on the current selection as-is — a selected pattern layer (its movable x/y/size square, see Tools → Select → Pattern squares) is copyable/cuttable/duplicable exactly like any other layer type, so all three are no-ops only when nothing is currently selected. Select All is the one deliberate exception: it still excludes pattern layers on purpose — a background-ish cover square joining every ⌘/Ctrl+A would make "select everything and move it" drag the background along too, so a pattern only joins a selection via a direct click (the two-tier hit-test) or the layer list. Select All doesn't read the current selection at all — it always selects every non-pattern layer in the document, so it's a no-op only when the document has no non-pattern layers.

Every one of these chords is registered with shift: false⌘⇧C never copies, matching the pre-refactor behavior exactly.

Where clones land

Both paste and duplicate share one clone technique (cloneNodeWithFreshIds from @zpd/core): fresh ids throughout each selected ordinary subtree, and a 2mm cascade offset applied to every leaf — so a repeated paste or duplicate never stacks exactly on top of its source. The v3 material tag preserves the source container; clones are appended inside that material and become the new selection. Fixed PCB roots themselves are never copied.

The OS clipboard envelope

Copy and cut write a versioned JSON envelope to the real OS clipboard via navigator.clipboard.writeText:

interface ZpdClipboardEnvelope {
  app: 'zpd';
  kind: 'layers';
  version: 3;
  layers: Array<{
    material: 'copper' | 'solder-mask' | 'silkscreen';
    node: LayerNode;
  }>;
}

This is what makes copy/paste work across zpd tabs — copy in one tab, switch to another, paste. Writing the envelope is best-effort: navigator.clipboard.writeText can reject, or the API can be entirely absent (e.g. an insecure context), and either case degrades silently to internal-clipboard-only — copy/cut/duplicate/select-all never fail outright over a clipboard permission issue.

On paste, the OS clipboard's text/plain payload is checked against the envelope markers and every ordinary node is revalidated through core's defensive fragment parser. v3 preserves each maximal selected root (leaf or group) with its owning material. Legacy v1 flat leaves and v2 ordinary trees remain accepted and are deterministically partitioned into the fixed material stack. Anything else — a URL, a sentence, unrelated JSON, or a future version — is left completely untouched; normal text paste is never hijacked.

Paste

⌘/Ctrl+V is deliberately not a keydown-handled shortcut. use-clipboard.ts exposes no handlePaste at all, so there is nothing a keyboard handler could wire a V case to — the editor's command registry lists an edit-paste entry purely as displayOnly (so the shortcuts overlay can still show "⌘V — Paste"), and its run() is never invoked. The real gesture rides the browser's native paste event, which use-clipboard.ts owns via a single window listener, checked in priority order:

  1. An image/SVG file on the OS clipboard — e.g. copied from an image viewer or another app. It goes through the same shared classifier as Add image… and drag-drop import: raster content becomes a design-time image, while real SVG can open the vector/material import dialog. An anonymous file with neither a name nor MIME type is content-sniffed before that decision.

  2. Text on the OS clipboard — parsed as a zpd envelope. A valid envelope pastes those layers (fresh ids, cascade offset, selected). Non-envelope text is left alone entirely, including skipping the internal-clipboard fallback below — the OS clipboard's current content always wins over a stale in-app copy. The one exception: if this session's own writeText from the most recent copy/cut hasn't resolved yet (a fast copy-then-paste can race the async write), the read text may simply predate that write — in that case paste falls through to the internal snapshot instead of silently doing nothing.

  3. The internal same-session clipboard — the last resort, used when the OS clipboard has neither an image nor any text at all (e.g. writeText was denied on copy).

Paste is ignored while an <input>, <textarea>, <select>, or a contenteditable element is focused, so a normal text paste into an editable field is never intercepted.