Skip to content

feat(canvas): Excalidraw read-only board + CanvasElement mapping (slices 1-2)#1407

Merged
jaylfc merged 4 commits into
devfrom
feat/canvas-excalidraw-slice2
Jun 23, 2026
Merged

feat(canvas): Excalidraw read-only board + CanvasElement mapping (slices 1-2)#1407
jaylfc merged 4 commits into
devfrom
feat/canvas-excalidraw-slice2

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 23, 2026

Copy link
Copy Markdown
Owner

First two slices of the tldraw to Excalidraw migration (engine = Excalidraw, CanvasElement stays canonical with Excalidraw as the view). Supersedes #1406 (its mapping is included here, with a strict-index type fix).

Slice 1: element-to-excalidraw.ts (pure mapping)

Projects a backend CanvasElement to an Excalidraw skeleton element (the convertToExcalidrawElements input), mirroring element-to-konva: note→coloured rectangle+label, link→labelled rectangle, image→image skeleton, text→text, mermaid/flowchart→rectangle labelled with the first source line (real diagram render is a later slice), mindmap_edge→arrow bound to from/to ids, unknown→generic rectangle. Skeleton types are a local faithful subset, so the mapping carries no Excalidraw runtime dependency.

Slice 2: ExcalidrawBoard.tsx (read-only board)

Adds @excalidraw/excalidraw (MIT). A read-only (viewMode + zenMode) board that renders the mapped scene; theme prop maps to Excalidraw light/dark. Built alongside the tldraw board, not yet wired into CanvasView (write-back, diagram render, and the swap are later slices), so it ships as harmless until the swap slice.

Tests

14 new (11 mapping unit tests + 3 mocked board smoke tests); full ProjectsApp suite 60 green; tsc -b clean.

Verification note

The board is not user-visible yet (not wired in). I will render it with sample data and screenshot on the Pi for visual review before the swap slice makes it default.

jaylfc added 2 commits June 23, 2026 15:18
First slice of the tldraw -> Excalidraw migration (canvas engine = Excalidraw,
CanvasElement stays canonical). element-to-excalidraw.ts is the pure view-side
projection from a backend CanvasElement to an Excalidraw skeleton element (the
convertToExcalidrawElements input shape), mirroring element-to-konva:
- note -> coloured rectangle + label
- link -> rectangle labelled with title/url
- image -> image skeleton carrying the file id
- text -> text element
- mermaid/flowchart -> rectangle labelled with the first source line (the
  diagram-render slice converts the real source later)
- mindmap_edge -> arrow bound to the from/to element ids
- user_shape/unknown -> generic rectangle

The skeleton types are a local faithful subset of ExcalidrawElementSkeleton, so
this pure mapping carries no @excalidraw/excalidraw runtime dependency; the board
slice wires the real package. 11 unit tests; malformed geometry coerces to
defaults rather than crashing the renderer.
… (slice 2)

Adds @excalidraw/excalidraw (MIT) and ExcalidrawBoard.tsx: a read-only
(viewMode + zenMode) Excalidraw view that renders the canonical CanvasElement
scene through element-to-excalidraw + convertToExcalidrawElements. Built
alongside the tldraw board, not yet wired into CanvasView; write-back
interactions, diagram rendering, and the swap that retires tldraw are later
slices. Theme prop maps to Excalidraw light/dark.

Smoke-tested with Excalidraw mocked (jsdom cannot run its canvas): asserts the
scene is mapped, soft-deleted elements dropped, read-only set, theme threaded.

Also fixes a strict-index (noUncheckedIndexedAccess) type error in the slice-1
note-background fallback.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 30 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 103c9fb3-45c4-4064-8ccd-fc30e8631ded

📥 Commits

Reviewing files that changed from the base of the PR and between 98ed0e2 and 318de71.

⛔ Files ignored due to path filters (1)
  • desktop/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • desktop/package.json
  • desktop/src/apps/ProjectsApp/__tests__/ExcalidrawBoard.test.tsx
  • desktop/src/apps/ProjectsApp/__tests__/element-to-excalidraw.test.ts
  • desktop/src/apps/ProjectsApp/canvas/ExcalidrawBoard.tsx
  • desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/canvas-excalidraw-slice2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Comment on lines +133 to +139
case "mindmap_edge":
return {
...base,
type: "arrow",
start: { id: str(p.from) },
end: { id: str(p.to) },
};

@gitar-bot gitar-bot Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Edge Case: Arrow bindings may reference missing/deleted element ids

mindmap_edge maps to an arrow with start: { id: str(p.from) } / end: { id: str(p.to) }. elementsToSkeletons filters out soft-deleted elements, but the arrow can still bind to a from/to id that was filtered out (soft-deleted) or never present in the scene. When the board is wired in, convertToExcalidrawElements resolves these bindings against the supplied elements and can warn/drop or otherwise misbehave when the referenced element is absent. Additionally, when p.from/p.to are missing, str() yields an empty-string id ({ id: "" }), which is never a valid binding target. Consider only emitting start/end when the referenced id is non-empty and present among the mapped elements (or omitting the binding otherwise). Low priority since the board is not yet wired into CanvasView.

Only bind arrow endpoints when the referenced id is non-empty.:

case "mindmap_edge": {
  const from = str(p.from);
  const to = str(p.to);
  return {
    ...base,
    type: "arrow",
    ...(from ? { start: { id: from } } : {}),
    ...(to ? { end: { id: to } } : {}),
  };
}

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Note

Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually.
Learn more about usage limits

Code Review 👍 Approved with suggestions 0 resolved / 1 findings

Implements the initial Excalidraw mapping and read-only board component, ensuring a clean migration path for CanvasElements. Ensure mindmap_edge logic accounts for potentially missing or deleted element IDs to prevent dangling arrow references.

💡 Edge Case: Arrow bindings may reference missing/deleted element ids

📄 desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:133-139 📄 desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:148-153

mindmap_edge maps to an arrow with start: { id: str(p.from) } / end: { id: str(p.to) }. elementsToSkeletons filters out soft-deleted elements, but the arrow can still bind to a from/to id that was filtered out (soft-deleted) or never present in the scene. When the board is wired in, convertToExcalidrawElements resolves these bindings against the supplied elements and can warn/drop or otherwise misbehave when the referenced element is absent. Additionally, when p.from/p.to are missing, str() yields an empty-string id ({ id: "" }), which is never a valid binding target. Consider only emitting start/end when the referenced id is non-empty and present among the mapped elements (or omitting the binding otherwise). Low priority since the board is not yet wired into CanvasView.

Only bind arrow endpoints when the referenced id is non-empty.
case "mindmap_edge": {
  const from = str(p.from);
  const to = str(p.to);
  return {
    ...base,
    type: "arrow",
    ...(from ? { start: { id: from } } : {}),
    ...(to ? { end: { id: to } } : {}),
  };
}
🤖 Prompt for agents
Code Review: Implements the initial Excalidraw mapping and read-only board component, ensuring a clean migration path for CanvasElements. Ensure mindmap_edge logic accounts for potentially missing or deleted element IDs to prevent dangling arrow references.

1. 💡 Edge Case: Arrow bindings may reference missing/deleted element ids
   Files: desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:133-139, desktop/src/apps/ProjectsApp/canvas/element-to-excalidraw.ts:148-153

   `mindmap_edge` maps to an arrow with `start: { id: str(p.from) }` / `end: { id: str(p.to) }`. `elementsToSkeletons` filters out soft-deleted elements, but the arrow can still bind to a `from`/`to` id that was filtered out (soft-deleted) or never present in the scene. When the board is wired in, `convertToExcalidrawElements` resolves these bindings against the supplied elements and can warn/drop or otherwise misbehave when the referenced element is absent. Additionally, when `p.from`/`p.to` are missing, `str()` yields an empty-string id (`{ id: "" }`), which is never a valid binding target. Consider only emitting `start`/`end` when the referenced id is non-empty and present among the mapped elements (or omitting the binding otherwise). Low priority since the board is not yet wired into CanvasView.

   Fix (Only bind arrow endpoints when the referenced id is non-empty.):
   case "mindmap_edge": {
     const from = str(p.from);
     const to = str(p.to);
     return {
       ...base,
       type: "arrow",
       ...(from ? { start: { id: from } } : {}),
       ...(to ? { end: { id: to } } : {}),
     };
   }

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Important

Your trial ends in 4 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

jaylfc added 2 commits June 23, 2026 15:35
Excalidraw opens at scroll origin, so a scene whose elements sit away from
(0,0) rendered off-screen. Capture the imperative API and call scrollToContent
with fitToContent once the API is ready and whenever the scene changes, so the
read-only board shows the mapped CanvasElements. Verified visually via a local
preview screenshot (notes/text/link/mermaid render).
Only emit an arrow start/end binding when its id is present. An empty-string
id (missing from/to) is never a valid Excalidraw binding target; an arrow with
no bindings renders as a free-floating line instead of misbehaving when the
referenced element is absent or soft-deleted.
@jaylfc jaylfc merged commit ff035f8 into dev Jun 23, 2026
7 of 8 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in TinyAgentOS Roadmap Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant