canvu-react 0.3.38 → 0.3.39

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.
package/dist/tldraw.js CHANGED
@@ -44,11 +44,76 @@ function createCustomShapeItem(id, bounds, content) {
44
44
  };
45
45
  }
46
46
 
47
+ // src/scene/link-item.ts
48
+ var LINK_PLUGIN_KEY = "canvuLink";
49
+ var LINK_CARD_BORDER = "#e2e8f0";
50
+ var LINK_CARD_ACCENT = "#2563eb";
51
+ var LINK_CARD_TITLE_COLOR = "#0f172a";
52
+ var LINK_CARD_TEXT_COLOR = "#475569";
53
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
54
+ var formatNumber = (value) => {
55
+ const rounded = Math.round(value * 100) / 100;
56
+ return Object.is(rounded, -0) ? "0" : String(rounded);
57
+ };
58
+ var escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
59
+ var escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
60
+ var getLinkHostname = (href) => {
61
+ try {
62
+ return new URL(href).hostname.replace(/^www\./, "");
63
+ } catch {
64
+ return href;
65
+ }
66
+ };
67
+ var buildLinkTextBand = (band) => {
68
+ const lineHeight = band.fontSize * 1.3;
69
+ const weight = band.fontWeight != null ? `font-weight:${band.fontWeight};` : "";
70
+ const lineClampStyle = band.clampLines ? `display:-webkit-box;-webkit-line-clamp:${band.clampLines};-webkit-box-orient:vertical;` : "";
71
+ 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>`;
72
+ };
73
+ function buildLinkCardSvg(width, height, link) {
74
+ const cardWidth = Math.max(1, width);
75
+ const cardHeight = Math.max(1, height);
76
+ const padding = 14;
77
+ const badgeSize = clamp(Math.min(72, cardHeight - padding * 2), 28, 96);
78
+ const textX = padding + badgeSize + 14;
79
+ const textWidth = Math.max(1, cardWidth - textX - padding);
80
+ const hostname = getLinkHostname(link.href);
81
+ const title = link.title?.trim() || hostname || "Link";
82
+ const description = link.description?.trim() || link.href;
83
+ const titleY = padding;
84
+ const titleHeight = clamp(cardHeight * 0.22, 18, 28);
85
+ const hostY = titleY + titleHeight + 2;
86
+ const hostHeight = 16;
87
+ const descY = hostY + hostHeight + 4;
88
+ const descHeight = Math.max(1, cardHeight - descY - padding);
89
+ 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>
90
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="12" fill="#f8fafc" stroke="${LINK_CARD_BORDER}" stroke-width="1" />
91
+ <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" />
92
+ <g transform="translate(${formatNumber(padding + badgeSize / 2)},${formatNumber(padding + badgeSize / 2)})" stroke="${LINK_CARD_ACCENT}" stroke-width="2.4" stroke-linecap="round" fill="none">
93
+ <path d="M-9 3 a6 6 0 0 1 0 -8 l4 -4 a6 6 0 0 1 8 8 l-2 2" />
94
+ <path d="M9 -3 a6 6 0 0 1 0 8 l-4 4 a6 6 0 0 1 -8 -8 l2 -2" />
95
+ </g>`;
96
+ const externalIconX = cardWidth - padding - 16;
97
+ 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">
98
+ <path d="M6 1 H11 V6" />
99
+ <path d="M11 1 L4.5 7.5" />
100
+ <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" />
101
+ </g>`;
102
+ return `
103
+ <rect width="${formatNumber(cardWidth)}" height="${formatNumber(cardHeight)}" rx="16" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1.5" />
104
+ ${badge}
105
+ ${externalIcon}
106
+ ${buildLinkTextBand({ x: textX, y: titleY, width: textWidth - 18, height: titleHeight, text: title, fontSize: 15, color: LINK_CARD_TITLE_COLOR, fontWeight: 700, clampLines: 1 })}
107
+ ${buildLinkTextBand({ x: textX, y: hostY, width: textWidth, height: hostHeight, text: hostname, fontSize: 12, color: LINK_CARD_ACCENT, clampLines: 1 })}
108
+ ${buildLinkTextBand({ x: textX, y: descY, width: textWidth, height: descHeight, text: description, fontSize: 12, color: LINK_CARD_TEXT_COLOR, clampLines: 2 })}
109
+ `;
110
+ }
111
+
47
112
  // src/scene/text-svg.ts
48
113
  function escapeSvgTextContent(s) {
49
114
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
50
115
  }
51
- function escapeHtmlText(s) {
116
+ function escapeHtmlText2(s) {
52
117
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
53
118
  }
54
119
  var DEFAULT_TEXT_FONT_SIZE = 18;
@@ -152,9 +217,9 @@ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#2563eb",
152
217
  const lh = lineHeightFor(fontSize);
153
218
  const trimmed = content.trim();
154
219
  if (trimmed.length === 0) {
155
- 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>`;
220
+ 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">${escapeHtmlText2(PLACEHOLDER)}</div></foreignObject>`;
156
221
  }
157
- const body = escapeHtmlText(content);
222
+ const body = escapeHtmlText2(content);
158
223
  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>`;
159
224
  }
160
225
 
@@ -831,10 +896,10 @@ function resolveColor(value) {
831
896
  function escapeXml(value) {
832
897
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
833
898
  }
834
- function escapeHtmlText2(value) {
899
+ function escapeHtmlText3(value) {
835
900
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
836
901
  }
837
- function formatNumber(value) {
902
+ function formatNumber2(value) {
838
903
  if (!Number.isFinite(value)) return "0";
839
904
  const rounded = Math.round(value * 100) / 100;
840
905
  return Number.isInteger(rounded) ? String(rounded) : String(rounded);
@@ -886,13 +951,13 @@ function sizeToFontPx(size) {
886
951
  function dashArrayForStyle(dash, strokeWidth) {
887
952
  if (!dash || dash === "solid") return void 0;
888
953
  if (dash === "dotted") {
889
- return `${formatNumber(Math.max(1.25, strokeWidth))} ${formatNumber(Math.max(2.5, strokeWidth * 2.2))}`;
954
+ return `${formatNumber2(Math.max(1.25, strokeWidth))} ${formatNumber2(Math.max(2.5, strokeWidth * 2.2))}`;
890
955
  }
891
956
  if (dash === "dashed") {
892
- return `${formatNumber(Math.max(5, strokeWidth * 4))} ${formatNumber(Math.max(3, strokeWidth * 2.2))}`;
957
+ return `${formatNumber2(Math.max(5, strokeWidth * 4))} ${formatNumber2(Math.max(3, strokeWidth * 2.2))}`;
893
958
  }
894
959
  if (dash === "draw") {
895
- return `${formatNumber(Math.max(3, strokeWidth * 2.4))} ${formatNumber(Math.max(2.4, strokeWidth * 1.6))}`;
960
+ return `${formatNumber2(Math.max(3, strokeWidth * 2.4))} ${formatNumber2(Math.max(2.4, strokeWidth * 1.6))}`;
896
961
  }
897
962
  return dash;
898
963
  }
@@ -908,11 +973,11 @@ function strokeAttrs(style) {
908
973
  const dashAttr = dashArray ? ` stroke-dasharray="${dashArray}"` : "";
909
974
  const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
910
975
  const lineJoin = style.lineJoin ? ` stroke-linejoin="${style.lineJoin}"` : "";
911
- return `stroke="${style.stroke}" stroke-width="${formatNumber(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
976
+ return `stroke="${style.stroke}" stroke-width="${formatNumber2(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
912
977
  }
913
978
  function wrapOpacity(svg, opacity) {
914
979
  if (opacity >= 0.999) return svg;
915
- return `<g opacity="${formatNumber(opacity)}">${svg}</g>`;
980
+ return `<g opacity="${formatNumber2(opacity)}">${svg}</g>`;
916
981
  }
917
982
  function buildForeignObjectTextSvg(options) {
918
983
  const x = options.x ?? 0;
@@ -927,8 +992,8 @@ function buildForeignObjectTextSvg(options) {
927
992
  const weight = options.fontWeight != null ? `font-weight:${options.fontWeight};` : "";
928
993
  const fontStyle = options.italic ? "font-style:italic;" : "";
929
994
  const background = options.background ? `background:${options.background};` : "";
930
- const radius = options.borderRadius != null ? `border-radius:${formatNumber(options.borderRadius)}px;` : "";
931
- 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>`;
995
+ const radius = options.borderRadius != null ? `border-radius:${formatNumber2(options.borderRadius)}px;` : "";
996
+ 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>`;
932
997
  }
933
998
  function richTextToPlainText(value) {
934
999
  const parts = [];
@@ -1456,17 +1521,17 @@ function createCustomImportedItem(snapshot, shape, localBounds, innerSvg, style)
1456
1521
  }
1457
1522
  function polygonPath(points) {
1458
1523
  if (points.length === 0) return "";
1459
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1524
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1460
1525
  for (let index = 1; index < points.length; index++) {
1461
1526
  const point = points[index];
1462
1527
  if (!point) continue;
1463
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1528
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1464
1529
  }
1465
1530
  return `${path} Z`;
1466
1531
  }
1467
1532
  function cloudPath(width, height) {
1468
1533
  const r = Math.min(width, height) * 0.15;
1469
- 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`;
1534
+ 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`;
1470
1535
  }
1471
1536
  function geoPath(geo, width, height) {
1472
1537
  if (geo === "diamond" || geo === "rhombus") {
@@ -1545,10 +1610,10 @@ function geoPath(geo, width, height) {
1545
1610
  ]);
1546
1611
  }
1547
1612
  if (geo === "check-box") {
1548
- 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)}`;
1613
+ 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)}`;
1549
1614
  }
1550
1615
  if (geo === "x-box") {
1551
- 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)}`;
1616
+ 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)}`;
1552
1617
  }
1553
1618
  return null;
1554
1619
  }
@@ -1564,7 +1629,7 @@ function renderGeoShape(snapshot, shape) {
1564
1629
  const text = extractPlainText(props);
1565
1630
  const fontSize = getNumber(props.fontSize) ?? sizeToFontPx(getString(props.size));
1566
1631
  const customPath = geoPath(geo, width, height);
1567
- 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" })} />`;
1632
+ 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" })} />`;
1568
1633
  const textMarkup = text ? buildForeignObjectTextSvg({
1569
1634
  width,
1570
1635
  height,
@@ -1649,11 +1714,11 @@ function renderStrokeShape(snapshot, shape) {
1649
1714
  }
1650
1715
  function polylinePath(points) {
1651
1716
  if (points.length === 0) return "";
1652
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1717
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1653
1718
  for (let index = 1; index < points.length; index++) {
1654
1719
  const point = points[index];
1655
1720
  if (!point) continue;
1656
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1721
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1657
1722
  }
1658
1723
  return path;
1659
1724
  }
@@ -1675,22 +1740,22 @@ function buildArrowHeadSvg(options) {
1675
1740
  const left = { x: bx + px * (headWidth / 2), y: by + py * (headWidth / 2) };
1676
1741
  const right = { x: bx - px * (headWidth / 2), y: by - py * (headWidth / 2) };
1677
1742
  if (type === "triangle") {
1678
- 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}" />`;
1743
+ 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}" />`;
1679
1744
  }
1680
1745
  if (type === "dot") {
1681
- return `<circle cx="${formatNumber(options.tip.x)}" cy="${formatNumber(options.tip.y)}" r="${formatNumber(Math.max(3, options.strokeWidth * 1.5))}" fill="${options.stroke}" />`;
1746
+ return `<circle cx="${formatNumber2(options.tip.x)}" cy="${formatNumber2(options.tip.y)}" r="${formatNumber2(Math.max(3, options.strokeWidth * 1.5))}" fill="${options.stroke}" />`;
1682
1747
  }
1683
1748
  if (type === "diamond") {
1684
1749
  const mid = {
1685
1750
  x: options.tip.x - ux * (headLength / 2),
1686
1751
  y: options.tip.y - uy * (headLength / 2)
1687
1752
  };
1688
- 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" />`;
1753
+ 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" />`;
1689
1754
  }
1690
1755
  if (type === "bar") {
1691
- 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" />`;
1756
+ 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" />`;
1692
1757
  }
1693
- 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" />`;
1758
+ 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" />`;
1694
1759
  }
1695
1760
  function renderLineShape(snapshot, shape) {
1696
1761
  const props = asRecord(shape.props) ?? {};
@@ -1759,7 +1824,7 @@ function renderArrowShape(snapshot, shape) {
1759
1824
  const translatedPoints = route.points.map(translate);
1760
1825
  const translatedStartRef = translate(route.startRef);
1761
1826
  const translatedEndRef = translate(route.endRef);
1762
- 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" })} />`;
1827
+ 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" })} />`;
1763
1828
  const startPoint = translatedPoints[0];
1764
1829
  const endPoint = translatedPoints[translatedPoints.length - 1];
1765
1830
  if (!startPoint || !endPoint) return null;
@@ -1791,9 +1856,9 @@ function renderMissingAssetPlaceholder(snapshot, shape, kind, detail) {
1791
1856
  const width = localBounds.width;
1792
1857
  const height = localBounds.height;
1793
1858
  const inner = `
1794
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="12" fill="#f4f4f5" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
1795
- <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" />
1796
- <circle cx="${formatNumber(width * 0.34)}" cy="${formatNumber(height * 0.34)}" r="${formatNumber(Math.max(6, Math.min(width, height) * 0.05))}" fill="#a1a1aa" />
1859
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#f4f4f5" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
1860
+ <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" />
1861
+ <circle cx="${formatNumber2(width * 0.34)}" cy="${formatNumber2(height * 0.34)}" r="${formatNumber2(Math.max(6, Math.min(width, height) * 0.05))}" fill="#a1a1aa" />
1797
1862
  ${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" })}
1798
1863
  ${buildForeignObjectTextSvg({ x: 14, y: height * 0.78, width: width - 28, height: height * 0.16, text: detail, fontSize: 12, color: "#52525b", align: "center", verticalAlign: "middle" })}
1799
1864
  `;
@@ -1847,10 +1912,10 @@ function renderVideoShape(snapshot, shape) {
1847
1912
  const label = getString(asRecord(asset?.props)?.name) ?? "Video";
1848
1913
  const subtitle = src ?? getString(props.assetId) ?? "Playback not supported in this importer";
1849
1914
  const inner = `
1850
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="14" fill="#18181b" stroke="#3f3f46" stroke-width="1.5" />
1851
- <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" />
1852
- <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" />
1853
- <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" />
1915
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="#18181b" stroke="#3f3f46" stroke-width="1.5" />
1916
+ <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" />
1917
+ <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" />
1918
+ <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" />
1854
1919
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.76, width: width - 32, height: height * 0.12, text: label, fontSize: 15, color: "#fafafa", fontWeight: 700 })}
1855
1920
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.88, width: width - 32, height: height * 0.08, text: subtitle, fontSize: 11, color: "#d4d4d8" })}
1856
1921
  `;
@@ -1876,8 +1941,8 @@ function renderNoteShape(snapshot, shape) {
1876
1941
  const fold = Math.min(width, height) * 0.14;
1877
1942
  const fontSize = sizeToFontPx(getString(props.size)) + numberOr(props.fontSizeAdjustment, 0);
1878
1943
  const inner = `
1879
- <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" />
1880
- <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" />
1944
+ <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" />
1945
+ <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" />
1881
1946
  ${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) })}
1882
1947
  `;
1883
1948
  return createCustomImportedItem(
@@ -1893,31 +1958,35 @@ function renderNoteShape(snapshot, shape) {
1893
1958
  }
1894
1959
  function renderBookmarkShape(snapshot, shape) {
1895
1960
  const props = asRecord(shape.props) ?? {};
1961
+ const asset = resolveAsset(snapshot, shape);
1962
+ const assetProps = asRecord(asset?.props) ?? {};
1896
1963
  const localBounds = resolveShapeLocalBounds(snapshot, shape.id);
1897
- const width = localBounds.width;
1898
- const height = localBounds.height;
1899
- const title = getString(props.title) ?? getString(props.label) ?? "Bookmark";
1900
- const url = getString(props.url) ?? "URL unavailable";
1901
- const description = getString(props.description) ?? getString(props.hostname) ?? "Imported from tldraw";
1902
- const stroke = shapeStrokeColor(shape);
1903
- const inner = `
1904
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="16" fill="#ffffff" stroke="${stroke}" stroke-width="1.5" />
1905
- <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" />
1906
- <path d="M${formatNumber(width * 0.11)} ${formatNumber(height * 0.25)} H${formatNumber(width * 0.18)}" stroke="${stroke}" stroke-width="3" stroke-linecap="round" />
1907
- ${buildForeignObjectTextSvg({ x: width * 0.28, y: 16, width: width * 0.64, height: height * 0.22, text: title, fontSize: 17, color: "#111827", fontWeight: 700 })}
1908
- ${buildForeignObjectTextSvg({ x: width * 0.28, y: height * 0.24, width: width * 0.64, height: height * 0.14, text: url, fontSize: 12, color: stroke })}
1909
- ${buildForeignObjectTextSvg({ x: 16, y: height * 0.46, width: width - 32, height: height * 0.42, text: description, fontSize: 13, color: "#4b5563" })}
1910
- `;
1911
- return createCustomImportedItem(
1964
+ const href = getString(props.url) ?? getString(assetProps.src) ?? getString(props.src) ?? "";
1965
+ const title = getString(assetProps.title) ?? getString(props.title) ?? getString(props.label) ?? void 0;
1966
+ const description = getString(assetProps.description) ?? getString(props.description) ?? void 0;
1967
+ const image = getString(assetProps.image) ?? getString(props.image) ?? void 0;
1968
+ const favicon = getString(assetProps.favicon) ?? getString(props.favicon) ?? void 0;
1969
+ const link = {
1970
+ href: href || "URL unavailable",
1971
+ ...title ? { title } : {},
1972
+ ...description ? { description } : {},
1973
+ ...image ? { image } : {},
1974
+ ...favicon ? { favicon } : {}
1975
+ };
1976
+ const inner = buildLinkCardSvg(localBounds.width, localBounds.height, link);
1977
+ const item = createCustomImportedItem(
1912
1978
  snapshot,
1913
1979
  shape,
1914
1980
  localBounds,
1915
- wrapOpacity(inner, shapeOpacity(shape)),
1916
- {
1917
- stroke,
1918
- strokeWidth: 1.5
1919
- }
1981
+ wrapOpacity(inner, shapeOpacity(shape))
1920
1982
  );
1983
+ return {
1984
+ ...item,
1985
+ pluginData: {
1986
+ ...item.pluginData ?? {},
1987
+ [LINK_PLUGIN_KEY]: link
1988
+ }
1989
+ };
1921
1990
  }
1922
1991
  function renderEmbedShape(snapshot, shape) {
1923
1992
  const props = asRecord(shape.props) ?? {};
@@ -1928,11 +1997,11 @@ function renderEmbedShape(snapshot, shape) {
1928
1997
  const title = getString(props.title) ?? getString(props.embedTitle) ?? "Embed";
1929
1998
  const url = getString(props.url) ?? getString(props.src) ?? "Embedded content";
1930
1999
  const inner = `
1931
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="16" fill="#0f172a" stroke="${stroke}" stroke-width="1.5" />
1932
- <rect x="16" y="16" width="${formatNumber(width - 32)}" height="${formatNumber(height * 0.62)}" rx="12" fill="#111827" stroke="#334155" stroke-width="1" />
1933
- <circle cx="${formatNumber(width * 0.14)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#ef4444" />
1934
- <circle cx="${formatNumber(width * 0.18)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#f59e0b" />
1935
- <circle cx="${formatNumber(width * 0.22)}" cy="${formatNumber(height * 0.14)}" r="5" fill="#22c55e" />
2000
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="16" fill="#0f172a" stroke="${stroke}" stroke-width="1.5" />
2001
+ <rect x="16" y="16" width="${formatNumber2(width - 32)}" height="${formatNumber2(height * 0.62)}" rx="12" fill="#111827" stroke="#334155" stroke-width="1" />
2002
+ <circle cx="${formatNumber2(width * 0.14)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#ef4444" />
2003
+ <circle cx="${formatNumber2(width * 0.18)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#f59e0b" />
2004
+ <circle cx="${formatNumber2(width * 0.22)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#22c55e" />
1936
2005
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.72, width: width - 36, height: height * 0.11, text: title, fontSize: 16, color: "#f8fafc", fontWeight: 700 })}
1937
2006
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.84, width: width - 36, height: height * 0.08, text: url, fontSize: 11, color: "#cbd5e1" })}
1938
2007
  `;
@@ -1955,8 +2024,8 @@ function renderFrameShape(snapshot, shape) {
1955
2024
  const stroke = shapeStrokeColor(shape);
1956
2025
  const title = (getString(props.name) ?? getString(props.title) ?? extractPlainText(props)) || "Frame";
1957
2026
  const inner = `
1958
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="14" fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="10 6" />
1959
- <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" />
2027
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="10 6" />
2028
+ <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" />
1960
2029
  ${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" })}
1961
2030
  `;
1962
2031
  return createCustomImportedItem(
@@ -1979,8 +2048,8 @@ function renderAnnotationShape(snapshot, shape) {
1979
2048
  const fill = shape.type === "annotationBubble" ? "#3b82f6" : resolveColor(getString(props.color) ?? "#ef4444");
1980
2049
  const text = shape.type === "label" ? extractPlainText(props) : String(getNumber(props.number) ?? "");
1981
2050
  const inner = `
1982
- <circle cx="${formatNumber(width / 2)}" cy="${formatNumber(height / 2)}" r="${formatNumber(radius)}" fill="${fill}" stroke="#ffffff" stroke-width="2" />
1983
- <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>
2051
+ <circle cx="${formatNumber2(width / 2)}" cy="${formatNumber2(height / 2)}" r="${formatNumber2(radius)}" fill="${fill}" stroke="#ffffff" stroke-width="2" />
2052
+ <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>
1984
2053
  `;
1985
2054
  return createCustomImportedItem(
1986
2055
  snapshot,
@@ -2000,7 +2069,7 @@ function renderUnsupportedShape(snapshot, shape, reason) {
2000
2069
  const width = localBounds.width;
2001
2070
  const height = localBounds.height;
2002
2071
  const inner = `
2003
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2072
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2004
2073
  ${buildForeignObjectTextSvg({ x: 12, y: 14, width: width - 24, height: 28, text: shape.type, fontSize: 14, color: "#111827", fontWeight: 700, verticalAlign: "middle" })}
2005
2074
  ${buildForeignObjectTextSvg({ x: 12, y: 50, width: width - 24, height: Math.max(20, height - 62), text: reason, fontSize: 12, color: "#52525b" })}
2006
2075
  `;