Skip to content

Copy and paste, pictures on the clipboard, and corners you can choose - #36

Merged
boubou666 merged 2 commits into
mainfrom
copy-paste-and-corners
Aug 8, 2026
Merged

boubou666 merged 2 commits into
mainfrom
copy-paste-and-corners

Conversation

@boubou666

@boubou666 boubou666 commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Two things: the clipboard, and corners you can choose. They arrived together because pasting a picture needed a new object type, and a new object type is the moment to notice that "how round are the corners" had never been anybody's decision.

The clipboard is the copy and paste events

Not navigator.clipboard. Reading the clipboard asynchronously needs a permission the browser prompts for, and a prompt in the middle of ⌘V is not paste. The event hands the data over with no permission at all, because the keystroke is the consent — and it is the only route by which an image on the clipboard arrives as a file rather than as nothing.

The listeners are on document, for the reason the board's shortcuts are on window: a canvas is a place where the thing you are acting on is selected rather than focused, so there is no element to hang them on. That means they hear the app's own chrome too, and isTyping is what keeps a copy inside the board title from copying the board instead.

I probed Chrome before designing around any of this, because two of its answers could have forced a hidden contenteditable — the trick other canvas apps use — and both went the right way: a document listener does fire with nothing focused and nothing selected, and what a copy handler writes does reach the real OS clipboard. That probe is now a test.

A selection travels as text/plain

A JSON envelope with a marker in it. The cost is visible and accepted — paste three cards into a text editor and you get JSON. The alternative is a custom clipboard type, which pastes into a text editor as nothing at all and is not carried between browsers or reliably between tabs; text is what makes a card copied on one board appear on another. Anything that is not ours is left to the browser rather than swallowed, so pasting a paragraph from a web page onto a board does nothing.

core/clipboard.js decides that a payload is ours and structurally sound. Board#paste decides whether the vocabulary is one this build speaks — the renderer builds an element by asking views for the type, so an object from a newer client would not draw wrong, it would throw and take the sheet's rendering with it.

Where a paste lands

Under the pointer when the pointer is over the board, and in the middle of the view when it is not. That is what makes pasting twice into two places one gesture rather than two, and the pointer is the only thing the person can be said to have pointed at.

Everything else follows rules the board already has:

  • New ids, always. The copy may be of objects still on this sheet, and two objects sharing an id is a document where an op means two things. Item ids inside a list are left alone, as Board#duplicate and Sheets#duplicate leave them.
  • An envelope brings what it holds, transitively, exactly as dragging one does. Copying a container that arrives empty is not what anybody selecting one meant.
  • Pasted envelopes go to the back, in their own order — the rule add follows, applied to a group. Appending one where it happens to fall in the copied order would drop it over the objects already on the board and bury them.
  • The whole paste is one undo entry, because it is one thing that happened.

An image is an object, and it carries the picture

{ type: 'image', x, y, w, h, src }, where src is a data URL rather than a link. The board is a document that is stored whole, broadcast to the other people on it and drawn to a PNG: three places a link would have to still resolve, from whatever network the reader happens to be on. Carrying the bytes means an image that is on the board is on the board.

The bytes being in the document is what makes the import a policy rather than a formality — core/image.js decides, platform/images.js enforces:

  • A source is a base64 data URL of a raster type, or it is not rendered. An object arrives over the board's channel from anyone authorised to edit it and goes into an img. A remote URL in there is a request this browser makes to a host of somebody else's choosing, which reports the reader's address and presence to it; svg+xml is a document rather than a bitmap. The same rule the style tokens follow. The attribute is removed rather than emptied, because src="" is a request for the page itself.
  • Bytes already small enough are kept exactly as they came. A screenshot pasted straight from the clipboard is crisp, lossless and usually small, and re-encoding it would store a lossy copy of itself for no benefit.
  • Anything larger is redrawn through a canvas as WebP, at most 1200px on its long side, retried smaller until it fits and refused out loud if it never does. A canvas also normalises: a type this app does not render becomes one it does, and the camera position a photograph was taken at is left behind, because a canvas has no metadata to give it.

The budget — around 180KB — is set by the channel, not the disk. Ops are broadcast, and an add carrying a whole picture is the only op that can reach a payload limit. Under it, pasting an image is a live edit like any other; over it, the send fails and the picture reaches everyone else on their next load. The failure is soft, which is exactly why it is worth spending some quality to stay under.

A file that cannot be made into a picture says so. The canvas layer reports a code and the shell writes the sentence, which is where every other message a person reads is written — so the board's export banner became a notice with a kind. One banner and not two: it is a fixed strip over the canvas, so a second would be drawn on top of the first.

Corners

corners: 'round' | 'square' as a token on the object, with the radius left in canvas.css — the split colour already uses. A card carrying corners: 8 is a card to find and rewrite the day the radius changes, and one carrying corners: 400 is a card nobody meant to make. The stylesheet needs one rule, because round is what every object has always been and stays the default: an object with no corners field renders exactly as it did before this existed.

It lives in its own module rather than in card-style.js, which is about what a card looks like and means nothing for an envelope. That is what lets the format bar offer the control to a selection with no cards in it — an envelope selected on its own now gets a bar with that one control, where before it got no bar at all. Each control is offered to whatever it means something for and to nothing else, which is the rule the bar was already following for a mixed selection.

Two buttons rather than one toggle, for the reason the alignment group has three: a selection where some objects are rounded and some are not has no true answer for a pressed state, and a toggle would have to invent one.

Images are in the corner vocabulary too. You named cards, envelopes and lists — images did not exist yet — and the alternative was a control that silently skipped one of the four things on a board.

The export draws all of it

radiusOf(obj) replaces the fixed 8, in four places. Pictures are the one thing that cannot be drawn in the pass they are read in — a 2D context has no way to wait for a bitmap — so every image is decoded first, keyed by source, and a source that will not decode is drawn as the empty box the DOM shows. The rest of the board is still worth a file, and drawing nothing would look like the export lost it.

What is deliberately not here

  • No cut. ⌘X is a third gesture with its own question — do the objects go when the copy is made or when the paste lands — and inventing an answer to that was not part of the request. Recorded in Not built yet as a choice rather than an oversight.
  • Nothing confirms a large image reached the channel. channel.send for a broadcast resolves before the socket has been written to, which is already documented two sections above it in the README; the picture is in the snapshot either way.
  • Images are the first thing big enough to reach the Web Storage quota. A refused write is already reported honestly, but nothing distinguishes "this browser is full" from "this write failed", and that difference is one a person can act on.

Tests

742 passing, line coverage 96.1%. Every new module at 100%: core/clipboard.js, core/corners.js, core/image.js, platform/clipboard.js, platform/images.js.

test/browser/clipboard.test.js drives the real system clipboard: Input.dispatchKeyEvent with commands: ['copy'] and ['paste'] runs the browser's own copy and paste, so a card copied and pasted really did go out to the OS and come back. That is the one thing a synthetic ClipboardEvent cannot check — that the handlers are reached at all — and it is the way this feature could be wholly broken with every other test in the file green. The details use synthetic events, including the image paste, since nothing can put a picture on the OS clipboard from a test.

Also covered: an envelope's contents travelling with it; a copy with nothing selected leaving the event alone; a copy inside a text field staying the field's; a paste landing under the pointer and in the middle of the view when there is none; foreign text doing nothing; a paste into a card being edited going in as text; images sized, cascaded and all selected; a clipboard holding both a picture and its file name taking the picture; a non-image file left to the browser; a refusal reported once; and a pasted picture surviving a reload.

test/node/board.test.js gained copyable and paste — the centring, the group keeping its shape, the new ids, the envelope depth, the unknown type dropped, and the single undo entry. test/browser/format-bar.test.js and renderer.test.js cover corners on all three existing types plus the guard on where a picture may come from. test/browser/export.test.js checks square corners reach the corner pixel and round ones do not, on a card, a list and an envelope, and that a picture is drawn, clipped, and replaced by its box when it will not decode.

One rename in the tests: the export banner's [data-export-error] is now [data-board-notice], since the banner is no longer only about exporting.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DGAkrDAKvwwHUFceiFYinQ

Summary by CodeRabbit

  • New Features
    • Added image support, including importing, resizing, displaying, and PNG exporting.
    • Added copy and paste for board objects and image files.
    • Added rounded or square corner controls for supported objects and mixed selections.
    • Added user-facing notices for export and image-paste issues.
  • Bug Fixes
    • Invalid or oversized images are rejected safely, with fallback rendering during export.
    • Clipboard actions preserve normal behavior in text fields and unsupported cases.
  • Documentation
    • Updated documentation covering images, corners, clipboard workflows, exporting, and known limitations.

Copy and paste the selection, paste an image from anywhere, and give any
object square corners instead of round ones.

**The clipboard** goes through the `copy` and `paste` events, not
`navigator.clipboard`: reading asynchronously needs a permission the browser
prompts for, and a prompt in the middle of ⌘V is not paste. The event hands the
data over because the keystroke is the consent — and it is the only route by
which an image arrives as a file rather than as nothing. On `document`, for the
reason the shortcuts are on `window`, so `isTyping` is what keeps a copy inside
the board title from copying the board.

A selection travels as `text/plain` holding a JSON envelope with a marker, which
is what makes a card copied on one board appear on another; anything that is not
ours is left to the browser. A paste lands under the pointer when there is one,
and follows the rules the board already has: new ids, envelopes bring what they
hold, pasted envelopes go to the back, and the whole paste is one undo entry.

**An image** is an object like any other, carrying the picture as a data URL
rather than a link — the document is stored whole, broadcast, and drawn to a
PNG, three places a link would have to still resolve. So the import is a policy:
a source is a base64 data URL of a raster type or it is not drawn at all, bytes
already small are kept exactly as they came, and anything larger is redrawn
through a canvas as WebP until it fits a budget set by the broadcast payload
limit. A file that cannot be made into a picture says so in the board's banner,
which is now a notice rather than an export error.

**Corners** are a token on the object with the radius left in `canvas.css`, in
their own module rather than in `card-style.js` — which is what lets the format
bar offer them to a selection with no cards in it. An envelope alone now gets a
bar with that one control, where before it got no bar at all.

The export draws all of it: pictures are decoded before painting, because a 2D
context cannot wait for a bitmap, and a source that will not decode is drawn as
the empty box the DOM shows rather than left out.

742 tests, 96.1% lines. The clipboard suite drives the real system clipboard
through CDP editing commands, so "the handlers are never reached" is a way this
could break that the tests would notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGAkrDAKvwwHUFceiFYinQ
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3650f2c2-29dd-468a-8be6-7648e19c113d

📥 Commits

Reviewing files that changed from the base of the PR and between e453f8f and a90f92f.

📒 Files selected for processing (4)
  • src/platform/clipboard.js
  • src/routes/BoardPage.jsx
  • test/browser/clipboard.test.js
  • test/node/board.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/node/board.test.js
  • src/routes/BoardPage.jsx
  • src/platform/clipboard.js
  • test/browser/clipboard.test.js

📝 Walkthrough

Walkthrough

This PR adds image objects, rounded or square corners, system clipboard copy/paste, image import and export support, generalized formatting controls, and unified board notices.

Changes

Board objects and clipboard features

Layer / File(s) Summary
Object contracts and board operations
src/core/board.js, src/core/clipboard.js, src/core/corners.js, src/core/image.js, test/node/*
Adds image defaults, corner resolution, validated clipboard payloads, deep-copy operations, centered paste placement, fresh IDs, and envelope handling.
System clipboard integration
src/platform/clipboard.js, src/app.js, src/components/BoardCanvas.jsx, test/browser/clipboard.test.js
Handles board copy/paste and image files, tracks paste placement, updates selection, reports rejected images, and disposes listeners during teardown.
Image import and rendering
src/platform/images.js, src/platform/views.js, src/platform/renderer.js, src/platform/export-png.js, test/browser/export.test.js, test/browser/renderer.test.js
Imports bounded raster images, validates sources, renders image objects, and exports images with decoded bitmaps and corner-aware clipping.
Corner formatting controls
src/components/FormatBar.jsx, src/core/bar-position.js, src/styles/canvas.css, test/browser/format-bar.test.js
Applies card properties and corner styles across eligible selections, including mixed selections and envelope controls.
Problem notices and documentation
src/routes/BoardPage.jsx, src/styles/shell.css, README.md
Adds shared board notices and documents image, corner, clipboard, and export behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BoardCanvas
  participant createClipboard
  participant createImageImport
  participant Board
  User->>BoardCanvas: Copy or paste
  BoardCanvas->>createClipboard: Handle clipboard event
  createClipboard->>Board: Read or write board objects
  createClipboard->>createImageImport: Decode image files
  createImageImport-->>createClipboard: Return bounded image data
  createClipboard->>Board: Insert and select pasted objects
Loading

Possibly related PRs

  • boubou666/peerthink#4: Shares the canvas application, board, and platform rendering integration extended by this PR.
  • boubou666/peerthink#9: Shares the PNG export pipeline extended for image objects and corner-aware rendering.
  • boubou666/peerthink#26: Shares the format-bar and positioning behavior extended for mixed selections and corner controls.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: clipboard copy/paste, image support, and selectable corner styles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copy-paste-and-corners

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
test/browser/renderer.test.js (1)

47-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add image to the corner-attribute loop.

The test name says every type. The loop covers card, envelope and list. test/node/corners.test.js:33 asserts that image is in CORNERED_TYPES, and this PR makes image renderable, so the one place the token is checked for every type should include it.

💚 Proposed test change
-    for (const type of ['card', 'envelope', 'list']) {
+    for (const type of ['card', 'envelope', 'list', 'image']) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/browser/renderer.test.js` around lines 47 - 54, Add image to the type
list in the corner-attribute test so the existing round and square
dataset.corners assertions also cover image, matching CORNERED_TYPES and the
newly renderable type.
src/core/clipboard.js (1)

54-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The structuredClone call in writeClipboard is redundant.

JSON.stringify already produces a detached string, so the caller cannot observe the objects afterwards. The clone adds a copy of every object and a new failure path: structuredClone throws DataCloneError on values that JSON.stringify silently drops, such as a function or a symbol on an object. The test at test/node/clipboard.test.js:38-43 passes either way, because it mutates after writeClipboard returns.

♻️ Proposed simplification
 export function writeClipboard(objects) {
   return JSON.stringify({
     kind: CLIPBOARD_KIND,
     v: CLIPBOARD_VERSION,
-    objects: objects.map((obj) => structuredClone(obj)),
+    objects,
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/clipboard.js` around lines 54 - 60, Remove the structuredClone call
from writeClipboard and pass each object directly to JSON.stringify. Preserve
the existing clipboard envelope and objects.map behavior while allowing
JSON.stringify to retain its current handling of unsupported values.
src/platform/clipboard.js (2)

161-164: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

getBoundingClientRect runs on every pointermove.

pointermove fires at pointer sampling rate, and getBoundingClientRect forces a layout read each time. The stage rect changes only on resize, scroll, or layout change. Cache the rect and refresh it on resize, or store the raw client coordinates and convert them in landing(), which runs once per paste.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/clipboard.js` around lines 161 - 164, Update the pointermove
handler near the stage listener to avoid calling stage.getBoundingClientRect()
for every event. Cache the stage rectangle and refresh it when the stage layout
can change, or store raw client coordinates and perform the coordinate
conversion in landing(), preserving the existing paste behavior.

144-158: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

A paste of several images creates one undo entry per image.

Each board.addCenteredOn call runs its own store.apply, so four pasted images need four presses of undo. Board#paste at src/core/board.js:145-147 deliberately batches its adds into a single apply for the opposite reason, and the PR objectives state that a paste creates one undo entry. The comment at Lines 155-156 already applies the "one thing that happened" rule to the selection, but not to undo.

Consider decoding all files first, then inserting them through one call, for example by building the image objects and passing them to board.paste, or by adding a board method that applies a batch of adds.

Based on the PR objectives: "Pasted objects receive new IDs, preserve nested envelope contents, land under the pointer or at the view center, and create one undo entry."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/clipboard.js` around lines 144 - 158, Update the multi-image
paste flow around board.addCenteredOn so all decoded images are inserted through
a single batched board operation, such as Board#paste or an equivalent batch-add
method, rather than invoking a separate store.apply per image. Preserve new IDs,
cascade positioning, refusal handling, and selection of all pasted images while
ensuring the entire paste creates exactly one undo entry.
src/core/board.js (1)

123-136: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider rejecting entries with non-numeric geometry in paste.

Line 124 only checks obj.type. If an entry has a missing or non-finite x, y, w, or h, bbox returns NaN bounds and the clones land at NaN coordinates, which then enters the document through store.apply. The clipboard path is safe today because readClipboard applies isPlaceable, but paste is a public core method and Board is described in the comment as the place where the vocabulary question is answered.

♻️ Proposed guard
-    const usable = objects.filter((obj) => OBJECT_DEFAULTS[obj?.type]);
+    const usable = objects.filter((obj) => OBJECT_DEFAULTS[obj?.type]
+      && [obj.x, obj.y, obj.w, obj.h].every((n) => Number.isFinite(n)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/board.js` around lines 123 - 136, Update Board.paste to filter out
objects whose x, y, w, or h values are not finite numbers, in addition to the
existing OBJECT_DEFAULTS type check. Ensure bbox and clone coordinate
calculations only receive valid geometry, while preserving the current
empty-result behavior when no usable objects remain.
test/node/clipboard.test.js (1)

51-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use CLIPBOARD_VERSION in place of the literal 1.

Lines 51 and 52 test "no objects" and "an empty payload". If CLIPBOARD_VERSION is raised to 2, both payloads become unknown-version payloads. They still read as null, so both tests keep passing, but they no longer exercise the object checks at src/core/clipboard.js:88-91.

♻️ Proposed change
-      ['a payload with no objects', JSON.stringify({ kind: CLIPBOARD_KIND, v: 1 })],
-      ['an empty payload', JSON.stringify({ kind: CLIPBOARD_KIND, v: 1, objects: [] })],
+      ['a payload with no objects', JSON.stringify({ kind: CLIPBOARD_KIND, v: CLIPBOARD_VERSION })],
+      ['an empty payload', JSON.stringify({ kind: CLIPBOARD_KIND, v: CLIPBOARD_VERSION, objects: [] })],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/node/clipboard.test.js` around lines 51 - 52, Replace the literal
clipboard version 1 in the “a payload with no objects” and “an empty payload”
test fixtures with CLIPBOARD_VERSION. Keep both payload shapes unchanged so
these tests continue exercising the object-presence checks in clipboard parsing.
test/browser/export.test.js (1)

333-341: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the outside probe.

at(39, 29) is the bottom-right pixel inside the 40 × 30 box, not a pixel outside it. The name states the opposite of what the assertion checks. Rename it to farCorner or similar so a later reader does not treat a passing assertion as proof that paint stopped at the box edge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/browser/export.test.js` around lines 333 - 341, Rename the `outside`
probe in the `is drawn, at the size its box says` test to `farCorner` or another
name indicating the bottom-right in-bounds pixel, and update the corresponding
assertion reference.
src/platform/images.js (1)

42-47: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Handle an aborted FileReader read.

dataUrlOf settles on load and error only. If the read is aborted, the promise never settles. read then never returns, and place in src/platform/clipboard.js stops without adding an object and without reporting a problem. Add an onabort handler that resolves null.

♻️ Proposed change
     const reader = new window.FileReader();
     reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : null);
     reader.onerror = () => resolve(null);
+    reader.onabort = () => resolve(null);
     reader.readAsDataURL(blob);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/images.js` around lines 42 - 47, Update dataUrlOf to handle
FileReader aborts by adding an onabort handler that resolves the promise with
null, matching the existing error behavior and ensuring aborted reads settle.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/clipboard.js`:
- Around line 66-78: Update onCopy to guard event.clipboardData before calling
setData, matching the existing data check in onPaste; return early when it is
null or unavailable while preserving the current typing and empty-selection
behavior.
- Around line 101-107: Update the file-drop handler’s place(files, landing())
flow to handle rejected promises from the conversion path. Attach rejection
handling that reports failures through onProblem and preserves processing of
remaining files, while keeping the event prevention and non-awaited handler
behavior intact.

In `@src/routes/BoardPage.jsx`:
- Around line 167-184: Update the export handler in BoardPage to capture the
current board generation before awaiting board.commands.exportPng(), then guard
every subsequent notice update, including success-result handling and the catch
path, so stale exports from a previous board cannot call setNotice after boardId
changes. Keep the existing messages and current-board availability behavior
unchanged.

In `@test/browser/clipboard.test.js`:
- Around line 93-116: Replace the fixed-sleep reliance in the clipboard tests
around the copy/paste assertions and the pasteText cases with a wait for the
expected app.store.order.length readiness condition, following the existing
pattern elsewhere in the file. Ensure each test waits until the pasted object
count is reached before calling objects(), selected(), or other store
assertions, while preserving the existing expected counts.

In `@test/browser/format-bar.test.js`:
- Around line 39-42: Update the absent helper’s fixed
double-requestAnimationFrame delay to wait for a state-specific DOM readiness
condition before evaluating absence. Ensure the condition reflects the relevant
React update for the provided selector, while preserving the existing app
readiness check and null-element assertion.

In `@test/node/board.test.js`:
- Around line 192-200: Update the test “nested data is copied, not shared” to
retain the original object passed through copied(), then mutate that source
object’s nested items after paste instead of applying a store patch. Assert
pasted.items remains unchanged, making the test verify that paste performs a
structured clone rather than sharing nested data.

---

Nitpick comments:
In `@src/core/board.js`:
- Around line 123-136: Update Board.paste to filter out objects whose x, y, w,
or h values are not finite numbers, in addition to the existing OBJECT_DEFAULTS
type check. Ensure bbox and clone coordinate calculations only receive valid
geometry, while preserving the current empty-result behavior when no usable
objects remain.

In `@src/core/clipboard.js`:
- Around line 54-60: Remove the structuredClone call from writeClipboard and
pass each object directly to JSON.stringify. Preserve the existing clipboard
envelope and objects.map behavior while allowing JSON.stringify to retain its
current handling of unsupported values.

In `@src/platform/clipboard.js`:
- Around line 161-164: Update the pointermove handler near the stage listener to
avoid calling stage.getBoundingClientRect() for every event. Cache the stage
rectangle and refresh it when the stage layout can change, or store raw client
coordinates and perform the coordinate conversion in landing(), preserving the
existing paste behavior.
- Around line 144-158: Update the multi-image paste flow around
board.addCenteredOn so all decoded images are inserted through a single batched
board operation, such as Board#paste or an equivalent batch-add method, rather
than invoking a separate store.apply per image. Preserve new IDs, cascade
positioning, refusal handling, and selection of all pasted images while ensuring
the entire paste creates exactly one undo entry.

In `@src/platform/images.js`:
- Around line 42-47: Update dataUrlOf to handle FileReader aborts by adding an
onabort handler that resolves the promise with null, matching the existing error
behavior and ensuring aborted reads settle.

In `@test/browser/export.test.js`:
- Around line 333-341: Rename the `outside` probe in the `is drawn, at the size
its box says` test to `farCorner` or another name indicating the bottom-right
in-bounds pixel, and update the corresponding assertion reference.

In `@test/browser/renderer.test.js`:
- Around line 47-54: Add image to the type list in the corner-attribute test so
the existing round and square dataset.corners assertions also cover image,
matching CORNERED_TYPES and the newly renderable type.

In `@test/node/clipboard.test.js`:
- Around line 51-52: Replace the literal clipboard version 1 in the “a payload
with no objects” and “an empty payload” test fixtures with CLIPBOARD_VERSION.
Keep both payload shapes unchanged so these tests continue exercising the
object-presence checks in clipboard parsing.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b62bdd2-a966-4bba-98c0-9620eb53ac5c

📥 Commits

Reviewing files that changed from the base of the PR and between fc8611b and e453f8f.

📒 Files selected for processing (25)
  • README.md
  • src/app.js
  • src/components/BoardCanvas.jsx
  • src/components/FormatBar.jsx
  • src/core/bar-position.js
  • src/core/board.js
  • src/core/clipboard.js
  • src/core/corners.js
  • src/core/image.js
  • src/platform/clipboard.js
  • src/platform/export-png.js
  • src/platform/images.js
  • src/platform/renderer.js
  • src/platform/views.js
  • src/routes/BoardPage.jsx
  • src/styles/canvas.css
  • src/styles/shell.css
  • test/browser/clipboard.test.js
  • test/browser/export.test.js
  • test/browser/format-bar.test.js
  • test/browser/renderer.test.js
  • test/node/board.test.js
  • test/node/clipboard.test.js
  • test/node/corners.test.js
  • test/node/image.test.js

Comment thread src/platform/clipboard.js
Comment thread src/platform/clipboard.js
Comment thread src/routes/BoardPage.jsx Outdated
Comment thread test/browser/clipboard.test.js
Comment thread test/browser/format-bar.test.js
Comment thread test/node/board.test.js
`nested data is copied, not shared` could not fail. It mutated `items` through
`store.apply`, which clones its own patch, so the assertion never touched the
array `paste` handed back — it passed whether or not anything was copied.
It mutates the source object now, and I broke `structuredClone` to watch it fail.

- **`onCopy` asks for `clipboardData` before reaching into it**, as `onPaste`
  already did. A `ClipboardEvent` the browser dispatched always carries one; a
  bare one from anything else on the page does not, and that would throw inside
  a document-level listener.
- **A file that cannot be placed is one refusal, not the end of the paste.**
  `images.read` answers null for the failures it expects, but it is a decoder
  and a canvas underneath and either can raise something else. Letting that out
  left the remaining files unplaced, said nothing to the person who pasted them,
  and became an unhandled rejection, because nobody is waiting on `place`.
- **An export reports on the board it exported.** `toBlob` on a large board is
  long enough to press back and open another one, and the banner would have
  turned up on a board that was never exported. Guarded by the board on screen,
  which is the rule the title read already follows.
- **Waits instead of sleeps** where there is a condition to wait for: the pasted
  objects landing, the words arriving in the field. "A paragraph does nothing"
  has no condition of its own, so it now pastes something that *does* land
  afterwards and counts what is there.

Left alone: `absent()` in the format-bar suite. Its two frames are the documented
answer to an assertion with no positive condition, and every call site already
waits for one — the bar appearing, or the selection reaching the renderer —
before asking what is missing.

742 tests, 96.1% lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGAkrDAKvwwHUFceiFYinQ
@boubou666
boubou666 merged commit 8b39083 into main Aug 8, 2026
3 checks passed
@boubou666
boubou666 deleted the copy-paste-and-corners branch August 8, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants