Skip to content

Commit 50c088d

Browse files
fix(frontend): harden tldraw v4 compatibility and clean docs
- split line shape props from arrow props for tldraw v4 schema validation - align root tldraw dependency with frontend v4.5 - add technology badges to README - remove emoji markers from README, docs, and MCP responses Co-Authored-By: AdaL <adal@sylph.ai>
1 parent f5c9ed0 commit 50c088d

6 files changed

Lines changed: 395 additions & 486 deletions

File tree

AGENTS.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,18 @@ These changes landed in tldraw v4.x and are **already handled** in `frontend/src
143143
Arrow labels were the last shape type still using plain `text`. In v4.0 they joined geo/text/note:
144144

145145
```typescript
146-
// Correctall shapes now use richText
146+
// Correct: all shapes use richText
147147
props: { richText: toRichText('my label') }
148148

149-
// ❌ Wrong — was valid in v3, breaks in v4
149+
// Incorrect: valid in v3, breaks in v4
150150
props: { text: 'my label' }
151151
```
152152

153153
### v4.0 — CSS variables renamed to `--tl-` prefix
154154
All tldraw CSS custom properties now start with `--tl-`. If you add custom CSS:
155155
```css
156-
/* v4 */ --tl-color-background: white;
157-
/* v3 */ --color-background: white;
156+
/* Correct in v4 */ --tl-color-background: white;
157+
/* Incorrect in v3 */ --color-background: white;
158158
```
159159

160160
### v4.2 — TipTap upgraded from v2 → v3
@@ -166,25 +166,25 @@ Our `toRichText()` helper generates plain ProseMirror doc JSON (no TipTap-specif
166166
### v4.3 — Custom shape type declaration pattern changed (TypeScript only)
167167
Runtime behavior unchanged — TypeScript only. Use module augmentation instead of `TLBaseShape`:
168168
```typescript
169-
// v4.3+
169+
// Correct: v4.3+
170170
declare module 'tldraw' {
171171
export interface TLGlobalShapePropsMap {
172172
'my-shape': { w: number; h: number }
173173
}
174174
}
175175
type MyShape = TLShape<'my-shape'>
176176

177-
// v4.2 and below
177+
// Incorrect: v4.2 and below
178178
type MyShape = TLBaseShape<'my-shape', { w: number; h: number }>
179179
```
180180

181181
### v4.4 — `options` prop consolidates `cameraOptions`/`textOptions`/`deepLinks`
182182
The standalone props are deprecated (still work). Prefer `options={{}}` going forward:
183183
```tsx
184-
// v4.4+ preferred
184+
// Correct: v4.4+ preferred
185185
<Tldraw options={{ camera: { isLocked: true }, deepLinks: true }} />
186186

187-
// 🔜 deprecated but still works
187+
// Deprecated but still works
188188
<Tldraw cameraOptions={{ isLocked: true }} deepLinks />
189189
```
190190

@@ -204,21 +204,21 @@ These were introduced in tldraw v3.10–v3.13 and are **already handled** in `fr
204204
tldraw validates `opacity` at the shape level alongside `x`, `y`, `isLocked`. Putting it inside `props` causes a `ValidationError: Unexpected property`.
205205

206206
```typescript
207-
// Correct
207+
// Correct
208208
editor.createShape({ id, type, x, y, opacity: 0.9, props: { color: 'blue', ... } })
209209

210-
// ❌ Wrong — causes ValidationError
210+
// Incorrect: causes ValidationError
211211
editor.createShape({ id, type, x, y, props: { color: 'blue', opacity: 0.9, ... } })
212212
```
213213

214214
### 2. `text``richText` on geo / text / note shapes (v3.10)
215215
The `text` string prop was removed from `geo`, `text`, and `note` shapes. Use `richText` with TipTap/ProseMirror JSON instead. **Arrow shapes still use plain `text` strings.**
216216

217217
```typescript
218-
// Correct — geo, text, note shapes
218+
// Correct — geo, text, note shapes
219219
props: { richText: toRichText('Hello\nWorld') }
220220

221-
// ❌ Wrong — causes ValidationError: Unexpected property
221+
// Incorrect: causes ValidationError: Unexpected property
222222
props: { text: 'Hello' }
223223
```
224224

@@ -231,7 +231,7 @@ The `toRichText()` helper in `App.tsx` converts plain strings (including `\n`) t
231231
Arrow `props.start` and `props.end` are now plain `{ x: number, y: number }` vectors. Connections to other shapes are **separate binding records** created via `editor.createBinding()`.
232232

233233
```typescript
234-
// Correct — v3 way
234+
// Correct — v3 way
235235
editor.createShape({ ..., props: { start: { x: 0, y: 0 }, end: { x: 200, y: 0 }, ... } })
236236
editor.createBinding({
237237
type: 'arrow',
@@ -240,7 +240,7 @@ editor.createBinding({
240240
props: { terminal: 'start', normalizedAnchor: { x: 0.5, y: 0.5 }, isExact: false, isPrecise: false }
241241
})
242242

243-
// ❌ Wrong — v2-style, causes ValidationError: Expected number, got undefined
243+
// Incorrect: v2-style, causes ValidationError: Expected number, got undefined
244244
props: { start: { type: 'binding', boundShapeId: '...', normalizedAnchor: ... } }
245245
```
246246

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# tldraw MCP Server
22

3-
> Programmatic canvas toolkit for AI agents — create, read, update, and delete tldraw shapes in real time via the Model Context Protocol.
4-
3+
> Programmatic canvas toolkit for AI agents - create, read, update, and delete tldraw shapes in real time via the Model Context Protocol.
4+
5+
[![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
6+
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7+
[![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black)](https://react.dev/)
8+
[![tldraw](https://img.shields.io/badge/tldraw-4.5-000000)](https://tldraw.dev/)
9+
[![MCP](https://img.shields.io/badge/Model_Context_Protocol-Server-5E5CE6)](https://modelcontextprotocol.io/)
10+
[![Express](https://img.shields.io/badge/Express-4.21-000000?logo=express&logoColor=white)](https://expressjs.com/)
11+
[![WebSocket](https://img.shields.io/badge/WebSocket-ws_8.18-010101)](https://github.com/websockets/ws)
512
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
613

714
## What It Is
@@ -134,7 +141,7 @@ codex mcp add tldraw \
134141

135142
## MCP Tools
136143

137-
### Implemented (17 tools)
144+
### Implemented (17 tools)
138145

139146
| Tool | Description |
140147
|------|-------------|
@@ -156,7 +163,7 @@ codex mcp add tldraw \
156163
| `align_elements` | Align multiple elements using an atomic batch update |
157164
| `distribute_elements` | Distribute multiple elements evenly using an atomic batch update |
158165

159-
### 🗺️ Roadmap
166+
### Roadmap
160167

161168
| Category | Tools | Status |
162169
|----------|-------|--------|

frontend/src/App.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,45 @@ function buildShapeProps(el: CanvasElement): Record<string, unknown> {
118118
}
119119
}
120120

121-
if (el.type === 'arrow' || el.type === 'line') {
121+
if (el.type === 'arrow') {
122122
const pts = el.points
123123
const last = pts ? pts[pts.length - 1] : null
124-
const props: Record<string, unknown> = {
124+
return {
125125
...base,
126+
labelColor: color,
127+
fill: el.fill ?? 'none',
126128
dash: el.dash ?? 'draw',
127129
size: el.size ?? 'm',
128130
arrowheadStart: el.startArrowhead ?? 'none',
129-
arrowheadEnd: el.endArrowhead ?? (el.type === 'arrow' ? 'arrow' : 'none'),
130-
richText: toRichText(el.text ?? ''),
131+
arrowheadEnd: el.endArrowhead ?? 'arrow',
131132
font: el.font ?? 'draw',
133+
kind: 'arc',
132134
start: { x: 0, y: 0 },
133135
end: last ? { x: last[0], y: last[1] } : { x: (el.endX ?? el.x + 200) - el.x, y: (el.endY ?? el.y) - el.y },
136+
bend: 0,
137+
richText: toRichText(el.text ?? ''),
138+
labelPosition: 0.5,
139+
scale: 1,
140+
elbowMidPoint: 0.5,
141+
}
142+
}
143+
144+
if (el.type === 'line') {
145+
const pts = el.points && el.points.length >= 2 ? el.points : [[0, 0], [(el.endX ?? el.x + 200) - el.x, (el.endY ?? el.y) - el.y]]
146+
const points = Object.fromEntries(
147+
pts.map(([x, y], i) => {
148+
const id = i === 0 ? 'start' : i === pts.length - 1 ? 'end' : `point:${i}`
149+
return [id, { id, index: `a${i + 1}`, x, y }]
150+
})
151+
)
152+
return {
153+
...base,
154+
dash: el.dash ?? 'draw',
155+
size: el.size ?? 'm',
156+
spline: 'line',
157+
points,
158+
scale: 1,
134159
}
135-
return props
136160
}
137161

138162
if (el.type === 'frame') {
@@ -212,8 +236,8 @@ function applyElement(editor: Editor, el: CanvasElement): void {
212236
})
213237
}
214238

215-
// tldraw v3: arrow bindings are separate records, not embedded in props
216-
if (el.type === 'arrow' || el.type === 'line') {
239+
// tldraw v3+: arrow bindings are separate records, not embedded in props
240+
if (el.type === 'arrow') {
217241
// Remove any existing bindings for this arrow
218242
const existingBindings = editor.getBindingsFromShape(shapeId, 'arrow')
219243
for (const b of existingBindings) {

0 commit comments

Comments
 (0)