canvu-react 0.3.38 → 0.3.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{asset-hydration-EtEuBwb7.d.cts → asset-hydration-B7yMDQE-.d.cts} +2 -2
  2. package/dist/{asset-hydration-DrTOgDdd.d.ts → asset-hydration-CbwQVAwh.d.ts} +2 -2
  3. package/dist/{camera-Di5R_Rwl.d.cts → camera-CVVG7z56.d.cts} +1 -1
  4. package/dist/{camera-AoTwBSoE.d.ts → camera-CoRYN_IV.d.ts} +1 -1
  5. package/dist/chatbot.d.cts +4 -4
  6. package/dist/chatbot.d.ts +4 -4
  7. package/dist/index.cjs +164 -15
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +6 -6
  10. package/dist/index.d.ts +6 -6
  11. package/dist/index.js +159 -16
  12. package/dist/index.js.map +1 -1
  13. package/dist/native.cjs +57 -14
  14. package/dist/native.cjs.map +1 -1
  15. package/dist/native.d.cts +2 -2
  16. package/dist/native.d.ts +2 -2
  17. package/dist/native.js +57 -14
  18. package/dist/native.js.map +1 -1
  19. package/dist/react.cjs +731 -258
  20. package/dist/react.cjs.map +1 -1
  21. package/dist/react.d.cts +10 -10
  22. package/dist/react.d.ts +10 -10
  23. package/dist/react.js +731 -258
  24. package/dist/react.js.map +1 -1
  25. package/dist/realtime.cjs +36 -0
  26. package/dist/realtime.cjs.map +1 -1
  27. package/dist/realtime.d.cts +6 -6
  28. package/dist/realtime.d.ts +6 -6
  29. package/dist/realtime.js +36 -0
  30. package/dist/realtime.js.map +1 -1
  31. package/dist/{shape-builders-CsbSRZnQ.d.cts → shape-builders-BAWu-PxX.d.cts} +46 -4
  32. package/dist/{shape-builders-CsSXKCcs.d.ts → shape-builders-ClKv9tz9.d.ts} +46 -4
  33. package/dist/tldraw.cjs +189 -78
  34. package/dist/tldraw.cjs.map +1 -1
  35. package/dist/tldraw.d.cts +1 -1
  36. package/dist/tldraw.d.ts +1 -1
  37. package/dist/tldraw.js +189 -78
  38. package/dist/tldraw.js.map +1 -1
  39. package/dist/{types-DWGk2_GZ.d.cts → types-BC9Xgfu6.d.cts} +20 -6
  40. package/dist/{types-Bnq2HtHQ.d.cts → types-BCCvY6ie.d.cts} +2 -0
  41. package/dist/{types-Bnq2HtHQ.d.ts → types-BCCvY6ie.d.ts} +2 -0
  42. package/dist/{types-B2Na677H.d.cts → types-BUPc2Zgw.d.cts} +1 -1
  43. package/dist/{types-zmUah-vP.d.ts → types-CYtq9Pr9.d.ts} +1 -1
  44. package/dist/{types-B6PAYKzx.d.ts → types-DlSVGX0w.d.ts} +20 -6
  45. package/package.json +1 -1
package/dist/tldraw.cjs CHANGED
@@ -50,19 +50,87 @@ function createCustomShapeItem(id, bounds, content) {
50
50
  };
51
51
  }
52
52
 
53
+ // src/scene/link-item.ts
54
+ var LINK_PLUGIN_KEY = "canvuLink";
55
+ var LINK_CARD_BORDER = "#e2e8f0";
56
+ var LINK_CARD_ACCENT = "#2563eb";
57
+ var LINK_CARD_TITLE_COLOR = "#0f172a";
58
+ var LINK_CARD_TEXT_COLOR = "#475569";
59
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
60
+ var formatNumber = (value) => {
61
+ const rounded = Math.round(value * 100) / 100;
62
+ return Object.is(rounded, -0) ? "0" : String(rounded);
63
+ };
64
+ var escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
65
+ var escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
66
+ var getLinkHostname = (href) => {
67
+ try {
68
+ return new URL(href).hostname.replace(/^www\./, "");
69
+ } catch {
70
+ return href;
71
+ }
72
+ };
73
+ var buildLinkTextBand = (band) => {
74
+ const lineHeight = band.fontSize * 1.3;
75
+ const weight = band.fontWeight != null ? `font-weight:${band.fontWeight};` : "";
76
+ const lineClampStyle = band.clampLines ? `display:-webkit-box;-webkit-line-clamp:${band.clampLines};-webkit-box-orient:vertical;` : "";
77
+ return `<foreignObject x="${formatNumber(band.x)}" y="${formatNumber(band.y)}" width="${formatNumber(Math.max(1, band.width))}" height="${formatNumber(Math.max(1, band.height))}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;font-family:system-ui,sans-serif;font-size:${formatNumber(band.fontSize)}px;line-height:${formatNumber(lineHeight)}px;color:${band.color};overflow:hidden;word-break:break-word;${lineClampStyle}${weight}">${escapeHtmlText(band.text)}</div></foreignObject>`;
78
+ };
79
+ function buildLinkCardSvg(width, height, link) {
80
+ const cardWidth = Math.max(1, width);
81
+ const cardHeight = Math.max(1, height);
82
+ const padding = 14;
83
+ const badgeSize = clamp(Math.min(72, cardHeight - padding * 2), 28, 96);
84
+ const textX = padding + badgeSize + 14;
85
+ const textWidth = Math.max(1, cardWidth - textX - padding);
86
+ const hostname = getLinkHostname(link.href);
87
+ const title = link.title?.trim() || hostname || "Link";
88
+ const description = link.description?.trim() || link.href;
89
+ const titleY = padding;
90
+ const titleHeight = clamp(cardHeight * 0.22, 18, 28);
91
+ const hostY = titleY + titleHeight + 2;
92
+ const hostHeight = 16;
93
+ const descY = hostY + hostHeight + 4;
94
+ const descHeight = Math.max(1, cardHeight - descY - padding);
95
+ const badge = link.favicon ? `<clipPath id="canvu-link-badge"><rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="12" /></clipPath>
96
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="12" fill="#f8fafc" stroke="${LINK_CARD_BORDER}" stroke-width="1" />
97
+ <image href="${escapeXmlAttribute(link.favicon)}" x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#canvu-link-badge)" />` : `<rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="12" fill="${LINK_CARD_ACCENT}" fill-opacity="0.1" stroke="${LINK_CARD_ACCENT}" stroke-opacity="0.3" stroke-width="1" />
98
+ <g transform="translate(${formatNumber(padding + badgeSize / 2)},${formatNumber(padding + badgeSize / 2)})" stroke="${LINK_CARD_ACCENT}" stroke-width="2.4" stroke-linecap="round" fill="none">
99
+ <path d="M-9 3 a6 6 0 0 1 0 -8 l4 -4 a6 6 0 0 1 8 8 l-2 2" />
100
+ <path d="M9 -3 a6 6 0 0 1 0 8 l-4 4 a6 6 0 0 1 -8 -8 l2 -2" />
101
+ </g>`;
102
+ const externalIconX = cardWidth - padding - 16;
103
+ const externalIcon = `<g transform="translate(${formatNumber(externalIconX)},${formatNumber(padding)})" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none">
104
+ <path d="M6 1 H11 V6" />
105
+ <path d="M11 1 L4.5 7.5" />
106
+ <path d="M9 7 V10 a1 1 0 0 1 -1 1 H2 a1 1 0 0 1 -1 -1 V4 a1 1 0 0 1 1 -1 H5" />
107
+ </g>`;
108
+ return `
109
+ <rect width="${formatNumber(cardWidth)}" height="${formatNumber(cardHeight)}" rx="16" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1.5" />
110
+ ${badge}
111
+ ${externalIcon}
112
+ ${buildLinkTextBand({ x: textX, y: titleY, width: textWidth - 18, height: titleHeight, text: title, fontSize: 15, color: LINK_CARD_TITLE_COLOR, fontWeight: 700, clampLines: 1 })}
113
+ ${buildLinkTextBand({ x: textX, y: hostY, width: textWidth, height: hostHeight, text: hostname, fontSize: 12, color: LINK_CARD_ACCENT, clampLines: 1 })}
114
+ ${buildLinkTextBand({ x: textX, y: descY, width: textWidth, height: descHeight, text: description, fontSize: 12, color: LINK_CARD_TEXT_COLOR, clampLines: 2 })}
115
+ `;
116
+ }
117
+
53
118
  // src/scene/text-svg.ts
54
119
  function escapeSvgTextContent(s) {
55
120
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
56
121
  }
57
- function escapeHtmlText(s) {
122
+ function escapeHtmlText2(s) {
58
123
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
59
124
  }
60
125
  var DEFAULT_TEXT_FONT_SIZE = 18;
126
+ var TEXT_FONT_FAMILY = "Inter, -apple-system, BlinkMacSystemFont, ui-sans-serif, system-ui, sans-serif";
61
127
  var LINE_HEIGHT_RATIO = 22 / 18;
62
- var FIRST_LINE_BASELINE_RATIO = 24 / 18;
128
+ var FIRST_LINE_BASELINE_RATIO = 20 / 18;
129
+ var EDIT_TOP_PAD_RATIO = 4 / 18;
130
+ var BOTTOM_PAD_RATIO = 4 / 18;
63
131
  var PLACEHOLDER = "Tap to type";
64
132
  var MIN_TEXT_BOX_W = 40;
65
- var MIN_TEXT_BOX_H = 36;
133
+ var MIN_TEXT_BOX_H = 26;
66
134
  var TEXT_PAD_X = 4;
67
135
  var MAX_TEXT_MEASURE_CACHE_ENTRIES = 2e3;
68
136
  var sharedMeasureContext;
@@ -108,7 +176,7 @@ function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
108
176
  let maxInnerW = 0;
109
177
  const ctx = getSharedMeasureContext();
110
178
  if (ctx) {
111
- ctx.font = `${fontSize}px system-ui, sans-serif`;
179
+ ctx.font = `${fontSize}px ${TEXT_FONT_FAMILY}`;
112
180
  for (const line of lines) {
113
181
  const toMeasure = trimmed.length === 0 ? PLACEHOLDER : line.length === 0 ? " " : line;
114
182
  maxInnerW = Math.max(maxInnerW, ctx.measureText(toMeasure).width);
@@ -124,22 +192,22 @@ function measureTextBoundsLocal(content, fontSize = DEFAULT_TEXT_FONT_SIZE) {
124
192
  const width = Math.max(minW, TEXT_PAD_X * 2 + maxInnerW);
125
193
  const height = Math.max(
126
194
  MIN_TEXT_BOX_H,
127
- baselineY + (lines.length - 1) * lh + Math.max(8, fontSize * 0.35)
195
+ baselineY + (lines.length - 1) * lh + Math.max(4, fontSize * BOTTOM_PAD_RATIO)
128
196
  );
129
197
  const measured = { width, height };
130
198
  cacheMeasuredBounds(cacheKey, measured);
131
199
  return measured;
132
200
  }
133
- function buildTextSvg(content, _width, _height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
201
+ function buildTextSvg(content, _width, _height, fillColor = "#1d1d1d", fontSize = DEFAULT_TEXT_FONT_SIZE) {
134
202
  const lh = lineHeightFor(fontSize);
135
203
  const y0 = firstLineBaselineY(fontSize);
136
204
  const trimmed = content.trim();
137
205
  if (trimmed.length === 0) {
138
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="#94a3b8" font-style="italic">${escapeSvgTextContent(PLACEHOLDER)}</text>`;
206
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="#94a3b8" font-style="italic">${escapeSvgTextContent(PLACEHOLDER)}</text>`;
139
207
  }
140
208
  const lines = content.split("\n");
141
209
  if (lines.length === 1) {
142
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${escapeSvgTextContent(lines[0] ?? "")}</text>`;
210
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="${fillColor}">${escapeSvgTextContent(lines[0] ?? "")}</text>`;
143
211
  }
144
212
  const parts = [];
145
213
  for (let i = 0; i < lines.length; i++) {
@@ -150,23 +218,24 @@ function buildTextSvg(content, _width, _height, fillColor = "#2563eb", fontSize
150
218
  parts.push(`<tspan x="4" dy="${lh}">${escapeSvgTextContent(line)}</tspan>`);
151
219
  }
152
220
  }
153
- return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="system-ui,sans-serif" fill="${fillColor}">${parts.join("")}</text>`;
221
+ return `<text x="4" y="${y0}" font-size="${fontSize}" font-family="${TEXT_FONT_FAMILY}" fill="${fillColor}">${parts.join("")}</text>`;
154
222
  }
155
- function buildTextFixedBoundsSvg(content, width, height, fillColor = "#2563eb", fontSize = DEFAULT_TEXT_FONT_SIZE) {
223
+ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#1d1d1d", fontSize = DEFAULT_TEXT_FONT_SIZE) {
156
224
  const w = Math.max(1, width);
157
225
  const h = Math.max(1, height);
158
226
  const lh = lineHeightFor(fontSize);
159
227
  const trimmed = content.trim();
228
+ const padTop = EDIT_TOP_PAD_RATIO * fontSize;
160
229
  if (trimmed.length === 0) {
161
- return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
230
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:#94a3b8;font-style:italic">${escapeHtmlText2(PLACEHOLDER)}</div></foreignObject>`;
162
231
  }
163
- const body = escapeHtmlText(content);
164
- return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:2px 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:system-ui,sans-serif;white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
232
+ const body = escapeHtmlText2(content);
233
+ return `<foreignObject width="${w}" height="${h}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${padTop}px 4px 0 4px;font-size:${fontSize}px;line-height:${lh}px;font-family:${TEXT_FONT_FAMILY};white-space:pre-wrap;word-wrap:break-word;overflow:hidden;color:${fillColor}">${body}</div></foreignObject>`;
165
234
  }
166
235
 
167
236
  // src/scene/shape-builders.ts
168
237
  var DEFAULT_STROKE_STYLE = {
169
- stroke: "#2563eb",
238
+ stroke: "#1d1d1d",
170
239
  strokeWidth: 2
171
240
  };
172
241
  function perfectFreehandOptions(toolKind, style, strokeComplete, pressureAware = false) {
@@ -217,7 +286,8 @@ function resolveStrokeStyle(item) {
217
286
  return {
218
287
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
219
288
  strokeWidth: item.strokeWidth ?? DEFAULT_STROKE_STYLE.strokeWidth,
220
- strokeOpacity: item.strokeOpacity
289
+ strokeOpacity: item.strokeOpacity,
290
+ strokeDash: item.strokeDash
221
291
  };
222
292
  }
223
293
  function strokeOpacityAttr(style) {
@@ -546,6 +616,30 @@ function buildDrawDotSvg(r, style = DEFAULT_STROKE_STYLE) {
546
616
  const op = style.strokeOpacity != null ? ` fill-opacity="${style.strokeOpacity}"` : "";
547
617
  return `<circle cx="${r}" cy="${r}" r="${r}" fill="${style.stroke}" shape-rendering="geometricPrecision"${op} />`;
548
618
  }
619
+ function dashArrayForDrawStroke(strokeWidth) {
620
+ const dash = Math.max(strokeWidth * 1.8, 4);
621
+ const gap = Math.max(strokeWidth * 1.4, 3);
622
+ return `${dash} ${gap}`;
623
+ }
624
+ function buildSmoothedCenterlinePath(points) {
625
+ if (points.length < 2) return null;
626
+ const first = points[0];
627
+ if (!first) return null;
628
+ let d = `M ${first.x} ${first.y}`;
629
+ for (let i = 1; i < points.length - 1; i++) {
630
+ const a = points[i];
631
+ const b = points[i + 1];
632
+ if (!a || !b) continue;
633
+ const midX = (a.x + b.x) / 2;
634
+ const midY = (a.y + b.y) / 2;
635
+ d += ` Q ${a.x} ${a.y} ${midX} ${midY}`;
636
+ }
637
+ const last = points[points.length - 1];
638
+ if (last) {
639
+ d += ` L ${last.x} ${last.y}`;
640
+ }
641
+ return d;
642
+ }
549
643
  function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeComplete = true) {
550
644
  if (pathPointsLocal.length === 0) return null;
551
645
  if (pathPointsLocal.length === 1) {
@@ -560,6 +654,18 @@ function computeFreehandSvgPayload(pathPointsLocal, style, toolKind, strokeCompl
560
654
  fillOpacity: style.strokeOpacity
561
655
  };
562
656
  }
657
+ if (style.strokeDash === "dashed" && (toolKind === "draw" || toolKind === "pencil")) {
658
+ const d2 = buildSmoothedCenterlinePath(pathPointsLocal);
659
+ if (!d2) return null;
660
+ return {
661
+ kind: "strokePath",
662
+ d: d2,
663
+ stroke: style.stroke,
664
+ strokeWidth: style.strokeWidth,
665
+ strokeOpacity: style.strokeOpacity,
666
+ strokeDasharray: dashArrayForDrawStroke(style.strokeWidth)
667
+ };
668
+ }
563
669
  const hasPressure = pathPointsLocal.some(
564
670
  (p) => p.pressure != null && Number.isFinite(p.pressure)
565
671
  );
@@ -602,7 +708,8 @@ function freehandPayloadToSvgString(payload) {
602
708
  strokeWidth: payload.strokeWidth,
603
709
  strokeOpacity: payload.strokeOpacity
604
710
  });
605
- return `<path d="${payload.d}" fill="none" stroke="${payload.stroke}" stroke-width="${payload.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op} />`;
711
+ const dash = payload.strokeDasharray ? ` stroke-dasharray="${payload.strokeDasharray}"` : "";
712
+ return `<path d="${payload.d}" fill="none" stroke="${payload.stroke}" stroke-width="${payload.strokeWidth}" stroke-linecap="round" stroke-linejoin="round" shape-rendering="geometricPrecision"${op}${dash} />`;
606
713
  }
607
714
  function buildFreehandPathSvg(pathPointsLocal, style, toolKind, strokeComplete = true) {
608
715
  const payload = computeFreehandSvgPayload(
@@ -837,10 +944,10 @@ function resolveColor(value) {
837
944
  function escapeXml(value) {
838
945
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
839
946
  }
840
- function escapeHtmlText2(value) {
947
+ function escapeHtmlText3(value) {
841
948
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
842
949
  }
843
- function formatNumber(value) {
950
+ function formatNumber2(value) {
844
951
  if (!Number.isFinite(value)) return "0";
845
952
  const rounded = Math.round(value * 100) / 100;
846
953
  return Number.isInteger(rounded) ? String(rounded) : String(rounded);
@@ -892,13 +999,13 @@ function sizeToFontPx(size) {
892
999
  function dashArrayForStyle(dash, strokeWidth) {
893
1000
  if (!dash || dash === "solid") return void 0;
894
1001
  if (dash === "dotted") {
895
- return `${formatNumber(Math.max(1.25, strokeWidth))} ${formatNumber(Math.max(2.5, strokeWidth * 2.2))}`;
1002
+ return `${formatNumber2(Math.max(1.25, strokeWidth))} ${formatNumber2(Math.max(2.5, strokeWidth * 2.2))}`;
896
1003
  }
897
1004
  if (dash === "dashed") {
898
- return `${formatNumber(Math.max(5, strokeWidth * 4))} ${formatNumber(Math.max(3, strokeWidth * 2.2))}`;
1005
+ return `${formatNumber2(Math.max(5, strokeWidth * 4))} ${formatNumber2(Math.max(3, strokeWidth * 2.2))}`;
899
1006
  }
900
1007
  if (dash === "draw") {
901
- return `${formatNumber(Math.max(3, strokeWidth * 2.4))} ${formatNumber(Math.max(2.4, strokeWidth * 1.6))}`;
1008
+ return `${formatNumber2(Math.max(3, strokeWidth * 2.4))} ${formatNumber2(Math.max(2.4, strokeWidth * 1.6))}`;
902
1009
  }
903
1010
  return dash;
904
1011
  }
@@ -914,11 +1021,11 @@ function strokeAttrs(style) {
914
1021
  const dashAttr = dashArray ? ` stroke-dasharray="${dashArray}"` : "";
915
1022
  const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
916
1023
  const lineJoin = style.lineJoin ? ` stroke-linejoin="${style.lineJoin}"` : "";
917
- return `stroke="${style.stroke}" stroke-width="${formatNumber(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
1024
+ return `stroke="${style.stroke}" stroke-width="${formatNumber2(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
918
1025
  }
919
1026
  function wrapOpacity(svg, opacity) {
920
1027
  if (opacity >= 0.999) return svg;
921
- return `<g opacity="${formatNumber(opacity)}">${svg}</g>`;
1028
+ return `<g opacity="${formatNumber2(opacity)}">${svg}</g>`;
922
1029
  }
923
1030
  function buildForeignObjectTextSvg(options) {
924
1031
  const x = options.x ?? 0;
@@ -933,8 +1040,8 @@ function buildForeignObjectTextSvg(options) {
933
1040
  const weight = options.fontWeight != null ? `font-weight:${options.fontWeight};` : "";
934
1041
  const fontStyle = options.italic ? "font-style:italic;" : "";
935
1042
  const background = options.background ? `background:${options.background};` : "";
936
- const radius = options.borderRadius != null ? `border-radius:${formatNumber(options.borderRadius)}px;` : "";
937
- return `<foreignObject x="${formatNumber(x)}" y="${formatNumber(y)}" width="${formatNumber(w)}" height="${formatNumber(h)}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${formatNumber(padding)}px;display:flex;align-items:${vertical};justify-content:${justify};text-align:${align};white-space:pre-wrap;word-break:break-word;overflow:hidden;color:${options.color};font-size:${formatNumber(options.fontSize)}px;line-height:${formatNumber(lineHeight)}px;font-family:system-ui,sans-serif;${weight}${fontStyle}${background}${radius}">${escapeHtmlText2(options.text)}</div></foreignObject>`;
1043
+ const radius = options.borderRadius != null ? `border-radius:${formatNumber2(options.borderRadius)}px;` : "";
1044
+ return `<foreignObject x="${formatNumber2(x)}" y="${formatNumber2(y)}" width="${formatNumber2(w)}" height="${formatNumber2(h)}"><div xmlns="http://www.w3.org/1999/xhtml" style="box-sizing:border-box;width:100%;height:100%;margin:0;padding:${formatNumber2(padding)}px;display:flex;align-items:${vertical};justify-content:${justify};text-align:${align};white-space:pre-wrap;word-break:break-word;overflow:hidden;color:${options.color};font-size:${formatNumber2(options.fontSize)}px;line-height:${formatNumber2(lineHeight)}px;font-family:system-ui,sans-serif;${weight}${fontStyle}${background}${radius}">${escapeHtmlText3(options.text)}</div></foreignObject>`;
938
1045
  }
939
1046
  function richTextToPlainText(value) {
940
1047
  const parts = [];
@@ -1462,17 +1569,17 @@ function createCustomImportedItem(snapshot, shape, localBounds, innerSvg, style)
1462
1569
  }
1463
1570
  function polygonPath(points) {
1464
1571
  if (points.length === 0) return "";
1465
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1572
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1466
1573
  for (let index = 1; index < points.length; index++) {
1467
1574
  const point = points[index];
1468
1575
  if (!point) continue;
1469
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1576
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1470
1577
  }
1471
1578
  return `${path} Z`;
1472
1579
  }
1473
1580
  function cloudPath(width, height) {
1474
1581
  const r = Math.min(width, height) * 0.15;
1475
- return `M${formatNumber(r)} ${formatNumber(height * 0.3)} Q0 ${formatNumber(height * 0.1)} ${formatNumber(width * 0.15)} ${formatNumber(height * 0.05)} Q${formatNumber(width * 0.3)} 0 ${formatNumber(width * 0.45)} ${formatNumber(height * 0.1)} Q${formatNumber(width * 0.7)} 0 ${formatNumber(width * 0.8)} ${formatNumber(height * 0.15)} Q${formatNumber(width)} ${formatNumber(height * 0.2)} ${formatNumber(width * 0.9)} ${formatNumber(height * 0.5)} Q${formatNumber(width)} ${formatNumber(height * 0.7)} ${formatNumber(width * 0.85)} ${formatNumber(height * 0.8)} Q${formatNumber(width * 0.7)} ${formatNumber(height)} ${formatNumber(width * 0.5)} ${formatNumber(height * 0.9)} Q${formatNumber(width * 0.3)} ${formatNumber(height)} ${formatNumber(width * 0.15)} ${formatNumber(height * 0.85)} Q0 ${formatNumber(height * 0.8)} ${formatNumber(r * 0.5)} ${formatNumber(height * 0.6)} Q0 ${formatNumber(height * 0.5)} ${formatNumber(r)} ${formatNumber(height * 0.3)} Z`;
1582
+ return `M${formatNumber2(r)} ${formatNumber2(height * 0.3)} Q0 ${formatNumber2(height * 0.1)} ${formatNumber2(width * 0.15)} ${formatNumber2(height * 0.05)} Q${formatNumber2(width * 0.3)} 0 ${formatNumber2(width * 0.45)} ${formatNumber2(height * 0.1)} Q${formatNumber2(width * 0.7)} 0 ${formatNumber2(width * 0.8)} ${formatNumber2(height * 0.15)} Q${formatNumber2(width)} ${formatNumber2(height * 0.2)} ${formatNumber2(width * 0.9)} ${formatNumber2(height * 0.5)} Q${formatNumber2(width)} ${formatNumber2(height * 0.7)} ${formatNumber2(width * 0.85)} ${formatNumber2(height * 0.8)} Q${formatNumber2(width * 0.7)} ${formatNumber2(height)} ${formatNumber2(width * 0.5)} ${formatNumber2(height * 0.9)} Q${formatNumber2(width * 0.3)} ${formatNumber2(height)} ${formatNumber2(width * 0.15)} ${formatNumber2(height * 0.85)} Q0 ${formatNumber2(height * 0.8)} ${formatNumber2(r * 0.5)} ${formatNumber2(height * 0.6)} Q0 ${formatNumber2(height * 0.5)} ${formatNumber2(r)} ${formatNumber2(height * 0.3)} Z`;
1476
1583
  }
1477
1584
  function geoPath(geo, width, height) {
1478
1585
  if (geo === "diamond" || geo === "rhombus") {
@@ -1551,10 +1658,10 @@ function geoPath(geo, width, height) {
1551
1658
  ]);
1552
1659
  }
1553
1660
  if (geo === "check-box") {
1554
- return `M0 0 H${formatNumber(width)} V${formatNumber(height)} H0 Z M${formatNumber(width * 0.2)} ${formatNumber(height * 0.56)} L${formatNumber(width * 0.42)} ${formatNumber(height * 0.78)} L${formatNumber(width * 0.82)} ${formatNumber(height * 0.24)}`;
1661
+ return `M0 0 H${formatNumber2(width)} V${formatNumber2(height)} H0 Z M${formatNumber2(width * 0.2)} ${formatNumber2(height * 0.56)} L${formatNumber2(width * 0.42)} ${formatNumber2(height * 0.78)} L${formatNumber2(width * 0.82)} ${formatNumber2(height * 0.24)}`;
1555
1662
  }
1556
1663
  if (geo === "x-box") {
1557
- return `M0 0 H${formatNumber(width)} V${formatNumber(height)} H0 Z M${formatNumber(width * 0.22)} ${formatNumber(height * 0.22)} L${formatNumber(width * 0.78)} ${formatNumber(height * 0.78)} M${formatNumber(width * 0.78)} ${formatNumber(height * 0.22)} L${formatNumber(width * 0.22)} ${formatNumber(height * 0.78)}`;
1664
+ return `M0 0 H${formatNumber2(width)} V${formatNumber2(height)} H0 Z M${formatNumber2(width * 0.22)} ${formatNumber2(height * 0.22)} L${formatNumber2(width * 0.78)} ${formatNumber2(height * 0.78)} M${formatNumber2(width * 0.78)} ${formatNumber2(height * 0.22)} L${formatNumber2(width * 0.22)} ${formatNumber2(height * 0.78)}`;
1558
1665
  }
1559
1666
  return null;
1560
1667
  }
@@ -1570,7 +1677,7 @@ function renderGeoShape(snapshot, shape) {
1570
1677
  const text = extractPlainText(props);
1571
1678
  const fontSize = getNumber(props.fontSize) ?? sizeToFontPx(getString(props.size));
1572
1679
  const customPath = geoPath(geo, width, height);
1573
- const shapeMarkup = geo === "ellipse" || geo === "oval" ? `<ellipse cx="${formatNumber(width / 2)}" cy="${formatNumber(height / 2)}" rx="${formatNumber(width / 2)}" ry="${formatNumber(height / 2)}" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : customPath ? `<path d="${customPath}" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : `<rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="8" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />`;
1680
+ const shapeMarkup = geo === "ellipse" || geo === "oval" ? `<ellipse cx="${formatNumber2(width / 2)}" cy="${formatNumber2(height / 2)}" rx="${formatNumber2(width / 2)}" ry="${formatNumber2(height / 2)}" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : customPath ? `<path d="${customPath}" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : `<rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="8" ${fillAttrs(getString(props.fill), stroke)} ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />`;
1574
1681
  const textMarkup = text ? buildForeignObjectTextSvg({
1575
1682
  width,
1576
1683
  height,
@@ -1655,11 +1762,11 @@ function renderStrokeShape(snapshot, shape) {
1655
1762
  }
1656
1763
  function polylinePath(points) {
1657
1764
  if (points.length === 0) return "";
1658
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1765
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1659
1766
  for (let index = 1; index < points.length; index++) {
1660
1767
  const point = points[index];
1661
1768
  if (!point) continue;
1662
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1769
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1663
1770
  }
1664
1771
  return path;
1665
1772
  }
@@ -1681,22 +1788,22 @@ function buildArrowHeadSvg(options) {
1681
1788
  const left = { x: bx + px * (headWidth / 2), y: by + py * (headWidth / 2) };
1682
1789
  const right = { x: bx - px * (headWidth / 2), y: by - py * (headWidth / 2) };
1683
1790
  if (type === "triangle") {
1684
- return `<polygon points="${formatNumber(options.tip.x)},${formatNumber(options.tip.y)} ${formatNumber(left.x)},${formatNumber(left.y)} ${formatNumber(right.x)},${formatNumber(right.y)}" fill="${options.stroke}" />`;
1791
+ return `<polygon points="${formatNumber2(options.tip.x)},${formatNumber2(options.tip.y)} ${formatNumber2(left.x)},${formatNumber2(left.y)} ${formatNumber2(right.x)},${formatNumber2(right.y)}" fill="${options.stroke}" />`;
1685
1792
  }
1686
1793
  if (type === "dot") {
1687
- return `<circle cx="${formatNumber(options.tip.x)}" cy="${formatNumber(options.tip.y)}" r="${formatNumber(Math.max(3, options.strokeWidth * 1.5))}" fill="${options.stroke}" />`;
1794
+ return `<circle cx="${formatNumber2(options.tip.x)}" cy="${formatNumber2(options.tip.y)}" r="${formatNumber2(Math.max(3, options.strokeWidth * 1.5))}" fill="${options.stroke}" />`;
1688
1795
  }
1689
1796
  if (type === "diamond") {
1690
1797
  const mid = {
1691
1798
  x: options.tip.x - ux * (headLength / 2),
1692
1799
  y: options.tip.y - uy * (headLength / 2)
1693
1800
  };
1694
- return `<polygon points="${formatNumber(options.tip.x)},${formatNumber(options.tip.y)} ${formatNumber(left.x)},${formatNumber(left.y)} ${formatNumber(mid.x - ux * (headLength / 2))},${formatNumber(mid.y - uy * (headLength / 2))} ${formatNumber(right.x)},${formatNumber(right.y)}" fill="none" stroke="${options.stroke}" stroke-width="${formatNumber(options.strokeWidth)}" stroke-linejoin="round" />`;
1801
+ return `<polygon points="${formatNumber2(options.tip.x)},${formatNumber2(options.tip.y)} ${formatNumber2(left.x)},${formatNumber2(left.y)} ${formatNumber2(mid.x - ux * (headLength / 2))},${formatNumber2(mid.y - uy * (headLength / 2))} ${formatNumber2(right.x)},${formatNumber2(right.y)}" fill="none" stroke="${options.stroke}" stroke-width="${formatNumber2(options.strokeWidth)}" stroke-linejoin="round" />`;
1695
1802
  }
1696
1803
  if (type === "bar") {
1697
- return `<line x1="${formatNumber(left.x)}" y1="${formatNumber(left.y)}" x2="${formatNumber(right.x)}" y2="${formatNumber(right.y)}" stroke="${options.stroke}" stroke-width="${formatNumber(options.strokeWidth)}" stroke-linecap="round" />`;
1804
+ return `<line x1="${formatNumber2(left.x)}" y1="${formatNumber2(left.y)}" x2="${formatNumber2(right.x)}" y2="${formatNumber2(right.y)}" stroke="${options.stroke}" stroke-width="${formatNumber2(options.strokeWidth)}" stroke-linecap="round" />`;
1698
1805
  }
1699
- return `<path d="M ${formatNumber(left.x)} ${formatNumber(left.y)} L ${formatNumber(options.tip.x)} ${formatNumber(options.tip.y)} L ${formatNumber(right.x)} ${formatNumber(right.y)}" fill="none" stroke="${options.stroke}" stroke-width="${formatNumber(options.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round" />`;
1806
+ return `<path d="M ${formatNumber2(left.x)} ${formatNumber2(left.y)} L ${formatNumber2(options.tip.x)} ${formatNumber2(options.tip.y)} L ${formatNumber2(right.x)} ${formatNumber2(right.y)}" fill="none" stroke="${options.stroke}" stroke-width="${formatNumber2(options.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round" />`;
1700
1807
  }
1701
1808
  function renderLineShape(snapshot, shape) {
1702
1809
  const props = asRecord(shape.props) ?? {};
@@ -1765,7 +1872,7 @@ function renderArrowShape(snapshot, shape) {
1765
1872
  const translatedPoints = route.points.map(translate);
1766
1873
  const translatedStartRef = translate(route.startRef);
1767
1874
  const translatedEndRef = translate(route.endRef);
1768
- const shaft = route.kind === "quadratic" && route.control ? `<path d="M${formatNumber(translatedPoints[0]?.x ?? 0)} ${formatNumber(translatedPoints[0]?.y ?? 0)} Q${formatNumber(route.control.x - localBounds.x)} ${formatNumber(route.control.y - localBounds.y)} ${formatNumber(translatedPoints[1]?.x ?? 0)} ${formatNumber(translatedPoints[1]?.y ?? 0)}" fill="none" ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : `<path d="${polylinePath(translatedPoints)}" fill="none" ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />`;
1875
+ const shaft = route.kind === "quadratic" && route.control ? `<path d="M${formatNumber2(translatedPoints[0]?.x ?? 0)} ${formatNumber2(translatedPoints[0]?.y ?? 0)} Q${formatNumber2(route.control.x - localBounds.x)} ${formatNumber2(route.control.y - localBounds.y)} ${formatNumber2(translatedPoints[1]?.x ?? 0)} ${formatNumber2(translatedPoints[1]?.y ?? 0)}" fill="none" ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />` : `<path d="${polylinePath(translatedPoints)}" fill="none" ${strokeAttrs({ stroke, strokeWidth, dash, lineCap: "round", lineJoin: "round" })} />`;
1769
1876
  const startPoint = translatedPoints[0];
1770
1877
  const endPoint = translatedPoints[translatedPoints.length - 1];
1771
1878
  if (!startPoint || !endPoint) return null;
@@ -1797,9 +1904,9 @@ function renderMissingAssetPlaceholder(snapshot, shape, kind, detail) {
1797
1904
  const width = localBounds.width;
1798
1905
  const height = localBounds.height;
1799
1906
  const inner = `
1800
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="12" fill="#f4f4f5" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
1801
- <path d="M${formatNumber(width * 0.18)} ${formatNumber(height * 0.7)} L${formatNumber(width * 0.38)} ${formatNumber(height * 0.48)} L${formatNumber(width * 0.56)} ${formatNumber(height * 0.62)} L${formatNumber(width * 0.76)} ${formatNumber(height * 0.32)} L${formatNumber(width * 0.84)} ${formatNumber(height * 0.4)} L${formatNumber(width * 0.84)} ${formatNumber(height * 0.82)} L${formatNumber(width * 0.18)} ${formatNumber(height * 0.82)} Z" fill="#d4d4d8" stroke="#a1a1aa" stroke-width="1" />
1802
- <circle cx="${formatNumber(width * 0.34)}" cy="${formatNumber(height * 0.34)}" r="${formatNumber(Math.max(6, Math.min(width, height) * 0.05))}" fill="#a1a1aa" />
1907
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#f4f4f5" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
1908
+ <path d="M${formatNumber2(width * 0.18)} ${formatNumber2(height * 0.7)} L${formatNumber2(width * 0.38)} ${formatNumber2(height * 0.48)} L${formatNumber2(width * 0.56)} ${formatNumber2(height * 0.62)} L${formatNumber2(width * 0.76)} ${formatNumber2(height * 0.32)} L${formatNumber2(width * 0.84)} ${formatNumber2(height * 0.4)} L${formatNumber2(width * 0.84)} ${formatNumber2(height * 0.82)} L${formatNumber2(width * 0.18)} ${formatNumber2(height * 0.82)} Z" fill="#d4d4d8" stroke="#a1a1aa" stroke-width="1" />
1909
+ <circle cx="${formatNumber2(width * 0.34)}" cy="${formatNumber2(height * 0.34)}" r="${formatNumber2(Math.max(6, Math.min(width, height) * 0.05))}" fill="#a1a1aa" />
1803
1910
  ${buildForeignObjectTextSvg({ x: 10, y: height * 0.04, width: width - 20, height: height * 0.28, text: kind, fontSize: 15, color: "#18181b", fontWeight: 700, align: "center", verticalAlign: "middle" })}
1804
1911
  ${buildForeignObjectTextSvg({ x: 14, y: height * 0.78, width: width - 28, height: height * 0.16, text: detail, fontSize: 12, color: "#52525b", align: "center", verticalAlign: "middle" })}
1805
1912
  `;
@@ -1853,10 +1960,10 @@ function renderVideoShape(snapshot, shape) {
1853
1960
  const label = getString(asRecord(asset?.props)?.name) ?? "Video";
1854
1961
  const subtitle = src ?? getString(props.assetId) ?? "Playback not supported in this importer";
1855
1962
  const inner = `
1856
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="14" fill="#18181b" stroke="#3f3f46" stroke-width="1.5" />
1857
- <rect x="${formatNumber(width * 0.04)}" y="${formatNumber(height * 0.08)}" width="${formatNumber(width * 0.92)}" height="${formatNumber(height * 0.66)}" rx="10" fill="#27272a" />
1858
- <circle cx="${formatNumber(width * 0.5)}" cy="${formatNumber(height * 0.41)}" r="${formatNumber(Math.min(width, height) * 0.12)}" fill="#fafafa" fill-opacity="0.92" />
1859
- <polygon points="${formatNumber(width * 0.48)},${formatNumber(height * 0.35)} ${formatNumber(width * 0.48)},${formatNumber(height * 0.47)} ${formatNumber(width * 0.57)},${formatNumber(height * 0.41)}" fill="#18181b" />
1963
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="#18181b" stroke="#3f3f46" stroke-width="1.5" />
1964
+ <rect x="${formatNumber2(width * 0.04)}" y="${formatNumber2(height * 0.08)}" width="${formatNumber2(width * 0.92)}" height="${formatNumber2(height * 0.66)}" rx="10" fill="#27272a" />
1965
+ <circle cx="${formatNumber2(width * 0.5)}" cy="${formatNumber2(height * 0.41)}" r="${formatNumber2(Math.min(width, height) * 0.12)}" fill="#fafafa" fill-opacity="0.92" />
1966
+ <polygon points="${formatNumber2(width * 0.48)},${formatNumber2(height * 0.35)} ${formatNumber2(width * 0.48)},${formatNumber2(height * 0.47)} ${formatNumber2(width * 0.57)},${formatNumber2(height * 0.41)}" fill="#18181b" />
1860
1967
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.76, width: width - 32, height: height * 0.12, text: label, fontSize: 15, color: "#fafafa", fontWeight: 700 })}
1861
1968
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.88, width: width - 32, height: height * 0.08, text: subtitle, fontSize: 11, color: "#d4d4d8" })}
1862
1969
  `;
@@ -1882,8 +1989,8 @@ function renderNoteShape(snapshot, shape) {
1882
1989
  const fold = Math.min(width, height) * 0.14;
1883
1990
  const fontSize = sizeToFontPx(getString(props.size)) + numberOr(props.fontSizeAdjustment, 0);
1884
1991
  const inner = `
1885
- <path d="M0 0 H${formatNumber(width - fold)} L${formatNumber(width)} ${formatNumber(fold)} V${formatNumber(height)} H0 Z" fill="${noteColor}" fill-opacity="0.22" stroke="${noteColor}" stroke-width="1.5" />
1886
- <path d="M${formatNumber(width - fold)} 0 V${formatNumber(fold)} H${formatNumber(width)}" fill="${noteColor}" fill-opacity="0.3" stroke="${noteColor}" stroke-width="1.5" />
1992
+ <path d="M0 0 H${formatNumber2(width - fold)} L${formatNumber2(width)} ${formatNumber2(fold)} V${formatNumber2(height)} H0 Z" fill="${noteColor}" fill-opacity="0.22" stroke="${noteColor}" stroke-width="1.5" />
1993
+ <path d="M${formatNumber2(width - fold)} 0 V${formatNumber2(fold)} H${formatNumber2(width)}" fill="${noteColor}" fill-opacity="0.3" stroke="${noteColor}" stroke-width="1.5" />
1887
1994
  ${buildForeignObjectTextSvg({ x: 12, y: 12, width: width - 24, height: height - 24, text, fontSize, color: labelColor, padding: 4, align: getString(props.align), verticalAlign: getString(props.verticalAlign) })}
1888
1995
  `;
1889
1996
  return createCustomImportedItem(
@@ -1899,31 +2006,35 @@ function renderNoteShape(snapshot, shape) {
1899
2006
  }
1900
2007
  function renderBookmarkShape(snapshot, shape) {
1901
2008
  const props = asRecord(shape.props) ?? {};
2009
+ const asset = resolveAsset(snapshot, shape);
2010
+ const assetProps = asRecord(asset?.props) ?? {};
1902
2011
  const localBounds = resolveShapeLocalBounds(snapshot, shape.id);
1903
- const width = localBounds.width;
1904
- const height = localBounds.height;
1905
- const title = getString(props.title) ?? getString(props.label) ?? "Bookmark";
1906
- const url = getString(props.url) ?? "URL unavailable";
1907
- const description = getString(props.description) ?? getString(props.hostname) ?? "Imported from tldraw";
1908
- const stroke = shapeStrokeColor(shape);
1909
- const inner = `
1910
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="16" fill="#ffffff" stroke="${stroke}" stroke-width="1.5" />
1911
- <rect x="16" y="16" width="${formatNumber(Math.max(56, width * 0.22))}" height="${formatNumber(Math.max(56, height * 0.32))}" rx="12" fill="${stroke}" fill-opacity="0.12" />
1912
- <path d="M${formatNumber(width * 0.11)} ${formatNumber(height * 0.25)} H${formatNumber(width * 0.18)}" stroke="${stroke}" stroke-width="3" stroke-linecap="round" />
1913
- ${buildForeignObjectTextSvg({ x: width * 0.28, y: 16, width: width * 0.64, height: height * 0.22, text: title, fontSize: 17, color: "#111827", fontWeight: 700 })}
1914
- ${buildForeignObjectTextSvg({ x: width * 0.28, y: height * 0.24, width: width * 0.64, height: height * 0.14, text: url, fontSize: 12, color: stroke })}
1915
- ${buildForeignObjectTextSvg({ x: 16, y: height * 0.46, width: width - 32, height: height * 0.42, text: description, fontSize: 13, color: "#4b5563" })}
1916
- `;
1917
- return createCustomImportedItem(
2012
+ const href = getString(props.url) ?? getString(assetProps.src) ?? getString(props.src) ?? "";
2013
+ const title = getString(assetProps.title) ?? getString(props.title) ?? getString(props.label) ?? void 0;
2014
+ const description = getString(assetProps.description) ?? getString(props.description) ?? void 0;
2015
+ const image = getString(assetProps.image) ?? getString(props.image) ?? void 0;
2016
+ const favicon = getString(assetProps.favicon) ?? getString(props.favicon) ?? void 0;
2017
+ const link = {
2018
+ href: href || "URL unavailable",
2019
+ ...title ? { title } : {},
2020
+ ...description ? { description } : {},
2021
+ ...image ? { image } : {},
2022
+ ...favicon ? { favicon } : {}
2023
+ };
2024
+ const inner = buildLinkCardSvg(localBounds.width, localBounds.height, link);
2025
+ const item = createCustomImportedItem(
1918
2026
  snapshot,
1919
2027
  shape,
1920
2028
  localBounds,
1921
- wrapOpacity(inner, shapeOpacity(shape)),
1922
- {
1923
- stroke,
1924
- strokeWidth: 1.5
1925
- }
2029
+ wrapOpacity(inner, shapeOpacity(shape))
1926
2030
  );
2031
+ return {
2032
+ ...item,
2033
+ pluginData: {
2034
+ ...item.pluginData ?? {},
2035
+ [LINK_PLUGIN_KEY]: link
2036
+ }
2037
+ };
1927
2038
  }
1928
2039
  function renderEmbedShape(snapshot, shape) {
1929
2040
  const props = asRecord(shape.props) ?? {};
@@ -1934,11 +2045,11 @@ function renderEmbedShape(snapshot, shape) {
1934
2045
  const title = getString(props.title) ?? getString(props.embedTitle) ?? "Embed";
1935
2046
  const url = getString(props.url) ?? getString(props.src) ?? "Embedded content";
1936
2047
  const inner = `
1937
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="16" fill="#0f172a" stroke="${stroke}" stroke-width="1.5" />
1938
- <rect x="16" y="16" width="${formatNumber(width - 32)}" height="${formatNumber(height * 0.62)}" rx="12" fill="#111827" stroke="#334155" stroke-width="1" />
1939
- <circle cx="${formatNumber(width * 0.14)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#ef4444" />
1940
- <circle cx="${formatNumber(width * 0.18)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#f59e0b" />
1941
- <circle cx="${formatNumber(width * 0.22)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#22c55e" />
2048
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="16" fill="#0f172a" stroke="${stroke}" stroke-width="1.5" />
2049
+ <rect x="16" y="16" width="${formatNumber2(width - 32)}" height="${formatNumber2(height * 0.62)}" rx="12" fill="#111827" stroke="#334155" stroke-width="1" />
2050
+ <circle cx="${formatNumber2(width * 0.14)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#ef4444" />
2051
+ <circle cx="${formatNumber2(width * 0.18)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#f59e0b" />
2052
+ <circle cx="${formatNumber2(width * 0.22)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#22c55e" />
1942
2053
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.72, width: width - 36, height: height * 0.11, text: title, fontSize: 16, color: "#f8fafc", fontWeight: 700 })}
1943
2054
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.84, width: width - 36, height: height * 0.08, text: url, fontSize: 11, color: "#cbd5e1" })}
1944
2055
  `;
@@ -1961,8 +2072,8 @@ function renderFrameShape(snapshot, shape) {
1961
2072
  const stroke = shapeStrokeColor(shape);
1962
2073
  const title = (getString(props.name) ?? getString(props.title) ?? extractPlainText(props)) || "Frame";
1963
2074
  const inner = `
1964
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="14" fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="10 6" />
1965
- <rect x="12" y="10" width="${formatNumber(Math.min(width - 24, Math.max(88, width * 0.34)))}" height="28" rx="8" fill="#ffffff" stroke="${stroke}" stroke-width="1.5" />
2075
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="10 6" />
2076
+ <rect x="12" y="10" width="${formatNumber2(Math.min(width - 24, Math.max(88, width * 0.34)))}" height="28" rx="8" fill="#ffffff" stroke="${stroke}" stroke-width="1.5" />
1966
2077
  ${buildForeignObjectTextSvg({ x: 16, y: 12, width: Math.min(width - 32, Math.max(80, width * 0.3)), height: 24, text: title, fontSize: 13, color: "#111827", fontWeight: 700, verticalAlign: "middle" })}
1967
2078
  `;
1968
2079
  return createCustomImportedItem(
@@ -1985,8 +2096,8 @@ function renderAnnotationShape(snapshot, shape) {
1985
2096
  const fill = shape.type === "annotationBubble" ? "#3b82f6" : resolveColor(getString(props.color) ?? "#ef4444");
1986
2097
  const text = shape.type === "label" ? extractPlainText(props) : String(getNumber(props.number) ?? "");
1987
2098
  const inner = `
1988
- <circle cx="${formatNumber(width / 2)}" cy="${formatNumber(height / 2)}" r="${formatNumber(radius)}" fill="${fill}" stroke="#ffffff" stroke-width="2" />
1989
- <text x="${formatNumber(width / 2)}" y="${formatNumber(height / 2 + 4)}" fill="#ffffff" font-size="10" font-weight="700" text-anchor="middle" dominant-baseline="central">${escapeXml(text)}</text>
2099
+ <circle cx="${formatNumber2(width / 2)}" cy="${formatNumber2(height / 2)}" r="${formatNumber2(radius)}" fill="${fill}" stroke="#ffffff" stroke-width="2" />
2100
+ <text x="${formatNumber2(width / 2)}" y="${formatNumber2(height / 2 + 4)}" fill="#ffffff" font-size="10" font-weight="700" text-anchor="middle" dominant-baseline="central">${escapeXml(text)}</text>
1990
2101
  `;
1991
2102
  return createCustomImportedItem(
1992
2103
  snapshot,
@@ -2006,7 +2117,7 @@ function renderUnsupportedShape(snapshot, shape, reason) {
2006
2117
  const width = localBounds.width;
2007
2118
  const height = localBounds.height;
2008
2119
  const inner = `
2009
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2120
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2010
2121
  ${buildForeignObjectTextSvg({ x: 12, y: 14, width: width - 24, height: 28, text: shape.type, fontSize: 14, color: "#111827", fontWeight: 700, verticalAlign: "middle" })}
2011
2122
  ${buildForeignObjectTextSvg({ x: 12, y: 50, width: width - 24, height: Math.max(20, height - 62), text: reason, fontSize: 12, color: "#52525b" })}
2012
2123
  `;