canvu-react 0.3.37 → 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.
@@ -1,4 +1,4 @@
1
- import { V as VectorSceneItem, L as LineEndpointsLocal, b as VectorPathPoint, R as Rect, a as ArrowBindings } from './types-Bnq2HtHQ.js';
1
+ import { V as VectorSceneItem, R as Rect, L as LineEndpointsLocal, b as VectorPathPoint, a as ArrowBindings } from './types-Bnq2HtHQ.js';
2
2
 
3
3
  /**
4
4
  * Kind of binary selected through the built-in canvu asset ingestion flow.
@@ -110,6 +110,44 @@ type VectorViewportAssetStore = {
110
110
  getHydrationRequest?: (item: VectorSceneItem) => VectorViewportAssetHydrationRequest | null;
111
111
  };
112
112
 
113
+ /** Plugin key under which link metadata is stored on a link item's `pluginData`. */
114
+ declare const LINK_PLUGIN_KEY = "canvuLink";
115
+ /**
116
+ * Metadata describing a clickable link/bookmark item.
117
+ * `href` is required; the rest enrich the rendered card (unfurl result).
118
+ */
119
+ type CanvuLinkData = {
120
+ /** Absolute URL the card points to. */
121
+ href: string;
122
+ /** Human readable title (falls back to the hostname). */
123
+ title?: string;
124
+ /** Short description shown under the title. */
125
+ description?: string;
126
+ /** Preview image URL (og:image). */
127
+ image?: string;
128
+ /** Favicon URL shown in the leading badge. */
129
+ favicon?: string;
130
+ };
131
+ /**
132
+ * Builds the inner SVG (no outer `<svg>`) for a link/bookmark card at the given box size.
133
+ * Uses fixed pixel bands so text never overlaps regardless of the card's aspect ratio.
134
+ */
135
+ declare function buildLinkCardSvg(width: number, height: number, link: CanvuLinkData): string;
136
+ /** Reads the link metadata from an item, or `null` when the item is not a link. */
137
+ declare function getLinkData(item: VectorSceneItem): CanvuLinkData | null;
138
+ /** True when the item carries link metadata (clickable card). */
139
+ declare function isLinkItem(item: VectorSceneItem): boolean;
140
+ /**
141
+ * Creates a clickable link/bookmark card item ready to append to the scene.
142
+ * The card resizes by scaling its authored SVG, like other custom shapes.
143
+ */
144
+ declare function createLinkItem(id: string, bounds: Rect, link: CanvuLinkData): VectorSceneItem;
145
+ /** Default card size used when placing a new link without explicit bounds. */
146
+ declare const DEFAULT_LINK_CARD_SIZE: {
147
+ readonly width: 360;
148
+ readonly height: 132;
149
+ };
150
+
113
151
  /** Resolved stroke for SVG generation. */
114
152
  type StrokeStyle = {
115
153
  stroke: string;
@@ -241,4 +279,4 @@ declare function createImageFromVectorTrace(id: string, bounds: Rect, imageVecto
241
279
  height: number;
242
280
  }): VectorSceneItem;
243
281
 
244
- export { type VectorViewportAssetResolveResult as A, type VectorViewportAssetUploadRequest as B, type VectorViewportAssetUploadResult as C, DEFAULT_STROKE_STYLE as D, type FreehandSvgPayload as F, type StrokeStyle as S, type VectorViewportAssetKind as V, applyStrokeToItem as a, buildArchitecturalCloudPathD as b, buildArchitecturalCloudSvg as c, buildArrowSvg as d, buildDrawDotSvg as e, buildEllipseSvg as f, buildFreehandPathSvg as g, buildLineSvg as h, buildRectSvg as i, computeFreehandSvgPayload as j, createArchitecturalCloudItem as k, createDrawDotItem as l, createEllipseItem as m, createFreehandStrokeItem as n, createImageFromVectorTrace as o, createImageItem as p, createLineItem as q, createRectangleItem as r, createShapeId as s, createTextItem as t, lineEndpointsToLocal as u, rebuildItemSvg as v, resolveStrokeStyle as w, type VectorViewportAssetStore as x, type VectorViewportAssetHydrationRequest as y, type VectorViewportAssetResolveRequest as z };
282
+ export { rebuildItemSvg as A, resolveStrokeStyle as B, type CanvuLinkData as C, DEFAULT_LINK_CARD_SIZE as D, type VectorViewportAssetStore as E, type FreehandSvgPayload as F, type VectorViewportAssetHydrationRequest as G, type VectorViewportAssetResolveRequest as H, type VectorViewportAssetResolveResult as I, type VectorViewportAssetUploadRequest as J, type VectorViewportAssetUploadResult as K, LINK_PLUGIN_KEY as L, type StrokeStyle as S, type VectorViewportAssetKind as V, DEFAULT_STROKE_STYLE as a, applyStrokeToItem as b, buildArchitecturalCloudPathD as c, buildArchitecturalCloudSvg as d, buildArrowSvg as e, buildDrawDotSvg as f, buildEllipseSvg as g, buildFreehandPathSvg as h, buildLineSvg as i, buildLinkCardSvg as j, buildRectSvg as k, computeFreehandSvgPayload as l, createArchitecturalCloudItem as m, createDrawDotItem as n, createEllipseItem as o, createFreehandStrokeItem as p, createImageFromVectorTrace as q, createImageItem as r, createLineItem as s, createLinkItem as t, createRectangleItem as u, createShapeId as v, createTextItem as w, getLinkData as x, isLinkItem as y, lineEndpointsToLocal as z };
@@ -1,4 +1,4 @@
1
- import { V as VectorSceneItem, L as LineEndpointsLocal, b as VectorPathPoint, R as Rect, a as ArrowBindings } from './types-Bnq2HtHQ.cjs';
1
+ import { V as VectorSceneItem, R as Rect, L as LineEndpointsLocal, b as VectorPathPoint, a as ArrowBindings } from './types-Bnq2HtHQ.cjs';
2
2
 
3
3
  /**
4
4
  * Kind of binary selected through the built-in canvu asset ingestion flow.
@@ -110,6 +110,44 @@ type VectorViewportAssetStore = {
110
110
  getHydrationRequest?: (item: VectorSceneItem) => VectorViewportAssetHydrationRequest | null;
111
111
  };
112
112
 
113
+ /** Plugin key under which link metadata is stored on a link item's `pluginData`. */
114
+ declare const LINK_PLUGIN_KEY = "canvuLink";
115
+ /**
116
+ * Metadata describing a clickable link/bookmark item.
117
+ * `href` is required; the rest enrich the rendered card (unfurl result).
118
+ */
119
+ type CanvuLinkData = {
120
+ /** Absolute URL the card points to. */
121
+ href: string;
122
+ /** Human readable title (falls back to the hostname). */
123
+ title?: string;
124
+ /** Short description shown under the title. */
125
+ description?: string;
126
+ /** Preview image URL (og:image). */
127
+ image?: string;
128
+ /** Favicon URL shown in the leading badge. */
129
+ favicon?: string;
130
+ };
131
+ /**
132
+ * Builds the inner SVG (no outer `<svg>`) for a link/bookmark card at the given box size.
133
+ * Uses fixed pixel bands so text never overlaps regardless of the card's aspect ratio.
134
+ */
135
+ declare function buildLinkCardSvg(width: number, height: number, link: CanvuLinkData): string;
136
+ /** Reads the link metadata from an item, or `null` when the item is not a link. */
137
+ declare function getLinkData(item: VectorSceneItem): CanvuLinkData | null;
138
+ /** True when the item carries link metadata (clickable card). */
139
+ declare function isLinkItem(item: VectorSceneItem): boolean;
140
+ /**
141
+ * Creates a clickable link/bookmark card item ready to append to the scene.
142
+ * The card resizes by scaling its authored SVG, like other custom shapes.
143
+ */
144
+ declare function createLinkItem(id: string, bounds: Rect, link: CanvuLinkData): VectorSceneItem;
145
+ /** Default card size used when placing a new link without explicit bounds. */
146
+ declare const DEFAULT_LINK_CARD_SIZE: {
147
+ readonly width: 360;
148
+ readonly height: 132;
149
+ };
150
+
113
151
  /** Resolved stroke for SVG generation. */
114
152
  type StrokeStyle = {
115
153
  stroke: string;
@@ -241,4 +279,4 @@ declare function createImageFromVectorTrace(id: string, bounds: Rect, imageVecto
241
279
  height: number;
242
280
  }): VectorSceneItem;
243
281
 
244
- export { type VectorViewportAssetResolveResult as A, type VectorViewportAssetUploadRequest as B, type VectorViewportAssetUploadResult as C, DEFAULT_STROKE_STYLE as D, type FreehandSvgPayload as F, type StrokeStyle as S, type VectorViewportAssetKind as V, applyStrokeToItem as a, buildArchitecturalCloudPathD as b, buildArchitecturalCloudSvg as c, buildArrowSvg as d, buildDrawDotSvg as e, buildEllipseSvg as f, buildFreehandPathSvg as g, buildLineSvg as h, buildRectSvg as i, computeFreehandSvgPayload as j, createArchitecturalCloudItem as k, createDrawDotItem as l, createEllipseItem as m, createFreehandStrokeItem as n, createImageFromVectorTrace as o, createImageItem as p, createLineItem as q, createRectangleItem as r, createShapeId as s, createTextItem as t, lineEndpointsToLocal as u, rebuildItemSvg as v, resolveStrokeStyle as w, type VectorViewportAssetStore as x, type VectorViewportAssetHydrationRequest as y, type VectorViewportAssetResolveRequest as z };
282
+ export { rebuildItemSvg as A, resolveStrokeStyle as B, type CanvuLinkData as C, DEFAULT_LINK_CARD_SIZE as D, type VectorViewportAssetStore as E, type FreehandSvgPayload as F, type VectorViewportAssetHydrationRequest as G, type VectorViewportAssetResolveRequest as H, type VectorViewportAssetResolveResult as I, type VectorViewportAssetUploadRequest as J, type VectorViewportAssetUploadResult as K, LINK_PLUGIN_KEY as L, type StrokeStyle as S, type VectorViewportAssetKind as V, DEFAULT_STROKE_STYLE as a, applyStrokeToItem as b, buildArchitecturalCloudPathD as c, buildArchitecturalCloudSvg as d, buildArrowSvg as e, buildDrawDotSvg as f, buildEllipseSvg as g, buildFreehandPathSvg as h, buildLineSvg as i, buildLinkCardSvg as j, buildRectSvg as k, computeFreehandSvgPayload as l, createArchitecturalCloudItem as m, createDrawDotItem as n, createEllipseItem as o, createFreehandStrokeItem as p, createImageFromVectorTrace as q, createImageItem as r, createLineItem as s, createLinkItem as t, createRectangleItem as u, createShapeId as v, createTextItem as w, getLinkData as x, isLinkItem as y, lineEndpointsToLocal as z };
package/dist/tldraw.cjs CHANGED
@@ -50,11 +50,76 @@ 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;
@@ -158,9 +223,9 @@ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#2563eb",
158
223
  const lh = lineHeightFor(fontSize);
159
224
  const trimmed = content.trim();
160
225
  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>`;
226
+ 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>`;
162
227
  }
163
- const body = escapeHtmlText(content);
228
+ const body = escapeHtmlText2(content);
164
229
  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>`;
165
230
  }
166
231
 
@@ -837,10 +902,10 @@ function resolveColor(value) {
837
902
  function escapeXml(value) {
838
903
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
839
904
  }
840
- function escapeHtmlText2(value) {
905
+ function escapeHtmlText3(value) {
841
906
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
842
907
  }
843
- function formatNumber(value) {
908
+ function formatNumber2(value) {
844
909
  if (!Number.isFinite(value)) return "0";
845
910
  const rounded = Math.round(value * 100) / 100;
846
911
  return Number.isInteger(rounded) ? String(rounded) : String(rounded);
@@ -892,13 +957,13 @@ function sizeToFontPx(size) {
892
957
  function dashArrayForStyle(dash, strokeWidth) {
893
958
  if (!dash || dash === "solid") return void 0;
894
959
  if (dash === "dotted") {
895
- return `${formatNumber(Math.max(1.25, strokeWidth))} ${formatNumber(Math.max(2.5, strokeWidth * 2.2))}`;
960
+ return `${formatNumber2(Math.max(1.25, strokeWidth))} ${formatNumber2(Math.max(2.5, strokeWidth * 2.2))}`;
896
961
  }
897
962
  if (dash === "dashed") {
898
- return `${formatNumber(Math.max(5, strokeWidth * 4))} ${formatNumber(Math.max(3, strokeWidth * 2.2))}`;
963
+ return `${formatNumber2(Math.max(5, strokeWidth * 4))} ${formatNumber2(Math.max(3, strokeWidth * 2.2))}`;
899
964
  }
900
965
  if (dash === "draw") {
901
- return `${formatNumber(Math.max(3, strokeWidth * 2.4))} ${formatNumber(Math.max(2.4, strokeWidth * 1.6))}`;
966
+ return `${formatNumber2(Math.max(3, strokeWidth * 2.4))} ${formatNumber2(Math.max(2.4, strokeWidth * 1.6))}`;
902
967
  }
903
968
  return dash;
904
969
  }
@@ -914,11 +979,11 @@ function strokeAttrs(style) {
914
979
  const dashAttr = dashArray ? ` stroke-dasharray="${dashArray}"` : "";
915
980
  const lineCap = style.lineCap ? ` stroke-linecap="${style.lineCap}"` : "";
916
981
  const lineJoin = style.lineJoin ? ` stroke-linejoin="${style.lineJoin}"` : "";
917
- return `stroke="${style.stroke}" stroke-width="${formatNumber(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
982
+ return `stroke="${style.stroke}" stroke-width="${formatNumber2(style.strokeWidth)}"${dashAttr}${lineCap}${lineJoin}`;
918
983
  }
919
984
  function wrapOpacity(svg, opacity) {
920
985
  if (opacity >= 0.999) return svg;
921
- return `<g opacity="${formatNumber(opacity)}">${svg}</g>`;
986
+ return `<g opacity="${formatNumber2(opacity)}">${svg}</g>`;
922
987
  }
923
988
  function buildForeignObjectTextSvg(options) {
924
989
  const x = options.x ?? 0;
@@ -933,8 +998,8 @@ function buildForeignObjectTextSvg(options) {
933
998
  const weight = options.fontWeight != null ? `font-weight:${options.fontWeight};` : "";
934
999
  const fontStyle = options.italic ? "font-style:italic;" : "";
935
1000
  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>`;
1001
+ const radius = options.borderRadius != null ? `border-radius:${formatNumber2(options.borderRadius)}px;` : "";
1002
+ 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
1003
  }
939
1004
  function richTextToPlainText(value) {
940
1005
  const parts = [];
@@ -1462,17 +1527,17 @@ function createCustomImportedItem(snapshot, shape, localBounds, innerSvg, style)
1462
1527
  }
1463
1528
  function polygonPath(points) {
1464
1529
  if (points.length === 0) return "";
1465
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1530
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1466
1531
  for (let index = 1; index < points.length; index++) {
1467
1532
  const point = points[index];
1468
1533
  if (!point) continue;
1469
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1534
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1470
1535
  }
1471
1536
  return `${path} Z`;
1472
1537
  }
1473
1538
  function cloudPath(width, height) {
1474
1539
  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`;
1540
+ 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
1541
  }
1477
1542
  function geoPath(geo, width, height) {
1478
1543
  if (geo === "diamond" || geo === "rhombus") {
@@ -1551,10 +1616,10 @@ function geoPath(geo, width, height) {
1551
1616
  ]);
1552
1617
  }
1553
1618
  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)}`;
1619
+ 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
1620
  }
1556
1621
  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)}`;
1622
+ 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
1623
  }
1559
1624
  return null;
1560
1625
  }
@@ -1570,7 +1635,7 @@ function renderGeoShape(snapshot, shape) {
1570
1635
  const text = extractPlainText(props);
1571
1636
  const fontSize = getNumber(props.fontSize) ?? sizeToFontPx(getString(props.size));
1572
1637
  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" })} />`;
1638
+ 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
1639
  const textMarkup = text ? buildForeignObjectTextSvg({
1575
1640
  width,
1576
1641
  height,
@@ -1655,11 +1720,11 @@ function renderStrokeShape(snapshot, shape) {
1655
1720
  }
1656
1721
  function polylinePath(points) {
1657
1722
  if (points.length === 0) return "";
1658
- let path = `M${formatNumber(points[0]?.x ?? 0)} ${formatNumber(points[0]?.y ?? 0)}`;
1723
+ let path = `M${formatNumber2(points[0]?.x ?? 0)} ${formatNumber2(points[0]?.y ?? 0)}`;
1659
1724
  for (let index = 1; index < points.length; index++) {
1660
1725
  const point = points[index];
1661
1726
  if (!point) continue;
1662
- path += ` L${formatNumber(point.x)} ${formatNumber(point.y)}`;
1727
+ path += ` L${formatNumber2(point.x)} ${formatNumber2(point.y)}`;
1663
1728
  }
1664
1729
  return path;
1665
1730
  }
@@ -1681,22 +1746,22 @@ function buildArrowHeadSvg(options) {
1681
1746
  const left = { x: bx + px * (headWidth / 2), y: by + py * (headWidth / 2) };
1682
1747
  const right = { x: bx - px * (headWidth / 2), y: by - py * (headWidth / 2) };
1683
1748
  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}" />`;
1749
+ 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
1750
  }
1686
1751
  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}" />`;
1752
+ 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
1753
  }
1689
1754
  if (type === "diamond") {
1690
1755
  const mid = {
1691
1756
  x: options.tip.x - ux * (headLength / 2),
1692
1757
  y: options.tip.y - uy * (headLength / 2)
1693
1758
  };
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" />`;
1759
+ 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
1760
  }
1696
1761
  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" />`;
1762
+ 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
1763
  }
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" />`;
1764
+ 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
1765
  }
1701
1766
  function renderLineShape(snapshot, shape) {
1702
1767
  const props = asRecord(shape.props) ?? {};
@@ -1765,7 +1830,7 @@ function renderArrowShape(snapshot, shape) {
1765
1830
  const translatedPoints = route.points.map(translate);
1766
1831
  const translatedStartRef = translate(route.startRef);
1767
1832
  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" })} />`;
1833
+ 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
1834
  const startPoint = translatedPoints[0];
1770
1835
  const endPoint = translatedPoints[translatedPoints.length - 1];
1771
1836
  if (!startPoint || !endPoint) return null;
@@ -1797,9 +1862,9 @@ function renderMissingAssetPlaceholder(snapshot, shape, kind, detail) {
1797
1862
  const width = localBounds.width;
1798
1863
  const height = localBounds.height;
1799
1864
  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" />
1865
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#f4f4f5" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
1866
+ <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" />
1867
+ <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
1868
  ${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
1869
  ${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
1870
  `;
@@ -1853,10 +1918,10 @@ function renderVideoShape(snapshot, shape) {
1853
1918
  const label = getString(asRecord(asset?.props)?.name) ?? "Video";
1854
1919
  const subtitle = src ?? getString(props.assetId) ?? "Playback not supported in this importer";
1855
1920
  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" />
1921
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="#18181b" stroke="#3f3f46" stroke-width="1.5" />
1922
+ <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" />
1923
+ <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" />
1924
+ <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
1925
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.76, width: width - 32, height: height * 0.12, text: label, fontSize: 15, color: "#fafafa", fontWeight: 700 })}
1861
1926
  ${buildForeignObjectTextSvg({ x: 16, y: height * 0.88, width: width - 32, height: height * 0.08, text: subtitle, fontSize: 11, color: "#d4d4d8" })}
1862
1927
  `;
@@ -1882,8 +1947,8 @@ function renderNoteShape(snapshot, shape) {
1882
1947
  const fold = Math.min(width, height) * 0.14;
1883
1948
  const fontSize = sizeToFontPx(getString(props.size)) + numberOr(props.fontSizeAdjustment, 0);
1884
1949
  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" />
1950
+ <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" />
1951
+ <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
1952
  ${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
1953
  `;
1889
1954
  return createCustomImportedItem(
@@ -1899,31 +1964,35 @@ function renderNoteShape(snapshot, shape) {
1899
1964
  }
1900
1965
  function renderBookmarkShape(snapshot, shape) {
1901
1966
  const props = asRecord(shape.props) ?? {};
1967
+ const asset = resolveAsset(snapshot, shape);
1968
+ const assetProps = asRecord(asset?.props) ?? {};
1902
1969
  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(
1970
+ const href = getString(props.url) ?? getString(assetProps.src) ?? getString(props.src) ?? "";
1971
+ const title = getString(assetProps.title) ?? getString(props.title) ?? getString(props.label) ?? void 0;
1972
+ const description = getString(assetProps.description) ?? getString(props.description) ?? void 0;
1973
+ const image = getString(assetProps.image) ?? getString(props.image) ?? void 0;
1974
+ const favicon = getString(assetProps.favicon) ?? getString(props.favicon) ?? void 0;
1975
+ const link = {
1976
+ href: href || "URL unavailable",
1977
+ ...title ? { title } : {},
1978
+ ...description ? { description } : {},
1979
+ ...image ? { image } : {},
1980
+ ...favicon ? { favicon } : {}
1981
+ };
1982
+ const inner = buildLinkCardSvg(localBounds.width, localBounds.height, link);
1983
+ const item = createCustomImportedItem(
1918
1984
  snapshot,
1919
1985
  shape,
1920
1986
  localBounds,
1921
- wrapOpacity(inner, shapeOpacity(shape)),
1922
- {
1923
- stroke,
1924
- strokeWidth: 1.5
1925
- }
1987
+ wrapOpacity(inner, shapeOpacity(shape))
1926
1988
  );
1989
+ return {
1990
+ ...item,
1991
+ pluginData: {
1992
+ ...item.pluginData ?? {},
1993
+ [LINK_PLUGIN_KEY]: link
1994
+ }
1995
+ };
1927
1996
  }
1928
1997
  function renderEmbedShape(snapshot, shape) {
1929
1998
  const props = asRecord(shape.props) ?? {};
@@ -1934,11 +2003,11 @@ function renderEmbedShape(snapshot, shape) {
1934
2003
  const title = getString(props.title) ?? getString(props.embedTitle) ?? "Embed";
1935
2004
  const url = getString(props.url) ?? getString(props.src) ?? "Embedded content";
1936
2005
  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" />
2006
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="16" fill="#0f172a" stroke="${stroke}" stroke-width="1.5" />
2007
+ <rect x="16" y="16" width="${formatNumber2(width - 32)}" height="${formatNumber2(height * 0.62)}" rx="12" fill="#111827" stroke="#334155" stroke-width="1" />
2008
+ <circle cx="${formatNumber2(width * 0.14)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#ef4444" />
2009
+ <circle cx="${formatNumber2(width * 0.18)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#f59e0b" />
2010
+ <circle cx="${formatNumber2(width * 0.22)}" cy="${formatNumber2(height * 0.14)}" r="5" fill="#22c55e" />
1942
2011
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.72, width: width - 36, height: height * 0.11, text: title, fontSize: 16, color: "#f8fafc", fontWeight: 700 })}
1943
2012
  ${buildForeignObjectTextSvg({ x: 18, y: height * 0.84, width: width - 36, height: height * 0.08, text: url, fontSize: 11, color: "#cbd5e1" })}
1944
2013
  `;
@@ -1961,8 +2030,8 @@ function renderFrameShape(snapshot, shape) {
1961
2030
  const stroke = shapeStrokeColor(shape);
1962
2031
  const title = (getString(props.name) ?? getString(props.title) ?? extractPlainText(props)) || "Frame";
1963
2032
  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" />
2033
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="14" fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="10 6" />
2034
+ <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
2035
  ${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
2036
  `;
1968
2037
  return createCustomImportedItem(
@@ -1985,8 +2054,8 @@ function renderAnnotationShape(snapshot, shape) {
1985
2054
  const fill = shape.type === "annotationBubble" ? "#3b82f6" : resolveColor(getString(props.color) ?? "#ef4444");
1986
2055
  const text = shape.type === "label" ? extractPlainText(props) : String(getNumber(props.number) ?? "");
1987
2056
  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>
2057
+ <circle cx="${formatNumber2(width / 2)}" cy="${formatNumber2(height / 2)}" r="${formatNumber2(radius)}" fill="${fill}" stroke="#ffffff" stroke-width="2" />
2058
+ <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
2059
  `;
1991
2060
  return createCustomImportedItem(
1992
2061
  snapshot,
@@ -2006,7 +2075,7 @@ function renderUnsupportedShape(snapshot, shape, reason) {
2006
2075
  const width = localBounds.width;
2007
2076
  const height = localBounds.height;
2008
2077
  const inner = `
2009
- <rect width="${formatNumber(width)}" height="${formatNumber(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2078
+ <rect width="${formatNumber2(width)}" height="${formatNumber2(height)}" rx="12" fill="#fafafa" stroke="#a1a1aa" stroke-width="1.5" stroke-dasharray="6 6" />
2010
2079
  ${buildForeignObjectTextSvg({ x: 12, y: 14, width: width - 24, height: 28, text: shape.type, fontSize: 14, color: "#111827", fontWeight: 700, verticalAlign: "middle" })}
2011
2080
  ${buildForeignObjectTextSvg({ x: 12, y: 50, width: width - 24, height: Math.max(20, height - 62), text: reason, fontSize: 12, color: "#52525b" })}
2012
2081
  `;