canvu-react 0.4.30 → 0.4.32

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/native.js CHANGED
@@ -29,11 +29,165 @@ function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
29
29
  return `<g transform="scale(${sx},${sy})">${inner}</g>`;
30
30
  }
31
31
 
32
+ // src/scene/link-item.ts
33
+ var LINK_PLUGIN_KEY = "canvuLink";
34
+ var DEFAULT_LINK_CARD_WIDTH = 320;
35
+ var DEFAULT_LINK_CARD_HEIGHT = 70;
36
+ var LINK_CARD_MIN_SCALE = 0.6;
37
+ var LINK_CARD_MAX_SCALE = 2.5;
38
+ var LINK_CARD_ASPECT = DEFAULT_LINK_CARD_WIDTH / DEFAULT_LINK_CARD_HEIGHT;
39
+ var LINK_CARD_BORDER = "oklch(0.918 0.008 255)";
40
+ var LINK_CARD_BORDER_STRONG = "oklch(0.86 0.012 255)";
41
+ var LINK_CARD_ACCENT = "oklch(0.55 0.19 264)";
42
+ var LINK_CARD_ACCENT_DEEP = "oklch(0.46 0.18 264)";
43
+ var LINK_CARD_TITLE_COLOR = "oklch(0.26 0.022 265)";
44
+ var LINK_CARD_TEXT_COLOR = "oklch(0.55 0.022 265)";
45
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
46
+ var formatNumber = (value) => {
47
+ const rounded = Math.round(value * 100) / 100;
48
+ return Object.is(rounded, -0) ? "0" : String(rounded);
49
+ };
50
+ var escapeXmlAttribute = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
51
+ var escapeHtmlText = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
52
+ var getLinkHostname = (href) => {
53
+ try {
54
+ return new URL(href).hostname.replace(/^www\./, "");
55
+ } catch {
56
+ return href;
57
+ }
58
+ };
59
+ var buildLinkTextBand = (band) => {
60
+ const lineHeight = band.height;
61
+ const weight = band.fontWeight != null ? `font-weight:${band.fontWeight};` : "";
62
+ 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;letter-spacing:-0.01em;color:${band.color};overflow:hidden;white-space:nowrap;text-overflow:ellipsis;${weight}">${escapeHtmlText(band.text)}</div></foreignObject>`;
63
+ };
64
+ var getLinkProtocol = (href) => {
65
+ try {
66
+ return new URL(href).protocol;
67
+ } catch {
68
+ return "";
69
+ }
70
+ };
71
+ var getLinkInitial = (hostname) => {
72
+ const first = hostname.trim().charAt(0).toUpperCase();
73
+ return first || "L";
74
+ };
75
+ var buildGoogleFaviconUrl = (hostname) => hostname ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=64` : null;
76
+ var getStableLinkIdSuffix = (value) => {
77
+ let hash = 0;
78
+ for (const char of value) {
79
+ hash = hash * 31 + char.charCodeAt(0) >>> 0;
80
+ }
81
+ return hash.toString(36);
82
+ };
83
+ function buildLinkCardSvg(width, _height, link) {
84
+ const cardWidth = Math.max(1, width);
85
+ const scale = cardWidth / DEFAULT_LINK_CARD_WIDTH;
86
+ const contentWidth = DEFAULT_LINK_CARD_WIDTH;
87
+ const contentHeight = DEFAULT_LINK_CARD_HEIGHT;
88
+ const padding = 14;
89
+ const badgeSize = 42;
90
+ const gap = 13;
91
+ const buttonSize = 34;
92
+ const textX = padding + badgeSize + gap;
93
+ const textWidth = Math.max(1, contentWidth - textX - buttonSize - gap - padding);
94
+ const hostname = getLinkHostname(link.href);
95
+ const title = link.title?.trim() || hostname || "Link";
96
+ const protocol = getLinkProtocol(link.href);
97
+ const subtitle = hostname || link.href;
98
+ const favicon = link.favicon?.trim() || buildGoogleFaviconUrl(hostname);
99
+ const idSuffix = getStableLinkIdSuffix(`${hostname}:${link.href}`);
100
+ const clipId = `canvu-link-favicon-${idSuffix}`;
101
+ const gradientId = `canvu-link-favicon-gradient-${idSuffix}`;
102
+ const buttonX = contentWidth - padding - buttonSize;
103
+ const buttonY = (contentHeight - buttonSize) / 2;
104
+ const isSecure = protocol === "https:";
105
+ const subtitleX = isSecure ? textX + 13 : textX;
106
+ const subtitleWidth = isSecure ? textWidth - 13 : textWidth;
107
+ const faviconImage = favicon ? `<image class="canvu-link-favicon-img" href="${escapeXmlAttribute(favicon)}" x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${clipId})" />` : "";
108
+ return `
109
+ <style>
110
+ .canvu-link-card-root .canvu-link-card { transition: transform .18s ease, filter .18s ease, stroke .18s ease; }
111
+ .canvu-link-card-root .canvu-link-open { opacity: 0; transform: translateX(-4px); transition: opacity .18s ease, transform .18s ease; }
112
+ .canvu-link-card-root:hover .canvu-link-card { transform: translateY(-2px); filter: drop-shadow(0 4px 14px oklch(0.4 0.05 265 / .08)) drop-shadow(0 1px 3px oklch(0.4 0.05 265 / .06)); stroke: ${LINK_CARD_BORDER_STRONG}; }
113
+ .canvu-link-card-root:hover .canvu-link-open { opacity: 1; transform: translateX(0); }
114
+ </style>
115
+ <g class="canvu-link-card-root" transform="scale(${formatNumber(scale)})">
116
+ <rect class="canvu-link-card" width="${formatNumber(contentWidth)}" height="${formatNumber(contentHeight)}" rx="16" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1" filter="drop-shadow(0 1px 2px oklch(0.4 0.03 265 / .05)) drop-shadow(0 1px 1px oklch(0.4 0.03 265 / .04))" />
117
+ <defs>
118
+ <linearGradient id="${gradientId}" x1="8" y1="48" x2="48" y2="8" gradientUnits="userSpaceOnUse">
119
+ <stop stop-color="${LINK_CARD_ACCENT}" />
120
+ <stop offset="1" stop-color="${LINK_CARD_ACCENT_DEEP}" />
121
+ </linearGradient>
122
+ <clipPath id="${clipId}">
123
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" />
124
+ </clipPath>
125
+ </defs>
126
+ <rect x="${formatNumber(padding)}" y="${formatNumber(padding)}" width="${formatNumber(badgeSize)}" height="${formatNumber(badgeSize)}" rx="11" fill="url(#${gradientId})" />
127
+ <text x="${formatNumber(padding + badgeSize / 2)}" y="${formatNumber(padding + badgeSize / 2 + 5)}" text-anchor="middle" font-family="system-ui,sans-serif" font-size="17" font-weight="700" fill="#ffffff">${escapeHtmlText(getLinkInitial(hostname))}</text>
128
+ ${faviconImage}
129
+ ${buildLinkTextBand({ x: textX, y: 16, width: textWidth, height: 19, text: title, fontSize: 14.5, color: LINK_CARD_TITLE_COLOR, fontWeight: 700 })}
130
+ ${isSecure ? `<g transform="translate(${formatNumber(textX)},40)" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" fill="none"><rect x="1.5" y="4.5" width="7" height="6" rx="1" /><path d="M3 4.5 V3 a2 2 0 0 1 4 0 v1.5" /></g>` : ""}
131
+ ${buildLinkTextBand({ x: subtitleX, y: 36, width: subtitleWidth, height: 17, text: subtitle, fontSize: 12.5, color: LINK_CARD_TEXT_COLOR })}
132
+ <g class="canvu-link-open" transform="translate(${formatNumber(buttonX)},${formatNumber(buttonY)})">
133
+ <rect width="${formatNumber(buttonSize)}" height="${formatNumber(buttonSize)}" rx="10" fill="#ffffff" stroke="${LINK_CARD_BORDER}" stroke-width="1" />
134
+ <g transform="translate(10,10)" stroke="${LINK_CARD_TEXT_COLOR}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none">
135
+ <path d="M8 2 H14 V8" />
136
+ <path d="M14 2 L7 9" />
137
+ <path d="M12 10 V13 a1 1 0 0 1 -1 1 H3 a1 1 0 0 1 -1 -1 V5 a1 1 0 0 1 1 -1 H6" />
138
+ </g>
139
+ </g>
140
+ </g>
141
+ `;
142
+ }
143
+ var isCanvuLinkData = (value) => {
144
+ if (!value || typeof value !== "object") return false;
145
+ const candidate = value;
146
+ return typeof candidate.href === "string" && candidate.href.length > 0;
147
+ };
148
+ function getLinkData(item) {
149
+ const entry = item.pluginData?.[LINK_PLUGIN_KEY];
150
+ return isCanvuLinkData(entry) ? entry : null;
151
+ }
152
+ function rebuildLinkItemSvg(item) {
153
+ const link = getLinkData(item);
154
+ if (!link) return item;
155
+ const scale = clamp(
156
+ item.bounds.width / DEFAULT_LINK_CARD_WIDTH,
157
+ LINK_CARD_MIN_SCALE,
158
+ LINK_CARD_MAX_SCALE
159
+ );
160
+ const width = DEFAULT_LINK_CARD_WIDTH * scale;
161
+ const height = DEFAULT_LINK_CARD_HEIGHT * scale;
162
+ const bounds = {
163
+ ...item.bounds,
164
+ width,
165
+ height
166
+ };
167
+ const customInnerSvg = buildLinkCardSvg(
168
+ DEFAULT_LINK_CARD_WIDTH,
169
+ DEFAULT_LINK_CARD_HEIGHT,
170
+ link
171
+ );
172
+ return {
173
+ ...item,
174
+ x: bounds.x,
175
+ y: bounds.y,
176
+ bounds,
177
+ customIntrinsicSize: {
178
+ width: DEFAULT_LINK_CARD_WIDTH,
179
+ height: DEFAULT_LINK_CARD_HEIGHT
180
+ },
181
+ customInnerSvg,
182
+ childrenSvg: buildLinkCardSvg(width, height, link)
183
+ };
184
+ }
185
+
32
186
  // src/scene/text-svg.ts
33
187
  function escapeSvgTextContent(s) {
34
188
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
35
189
  }
36
- function escapeHtmlText(s) {
190
+ function escapeHtmlText2(s) {
37
191
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
38
192
  }
39
193
  var DEFAULT_TEXT_FONT_SIZE = 18;
@@ -148,9 +302,9 @@ function buildTextFixedBoundsSvg(content, width, height, fillColor = "#1d1d1d",
148
302
  const trimmed = content.trim();
149
303
  const padTop = EDIT_TOP_PAD_RATIO * fontSize;
150
304
  if (trimmed.length === 0) {
151
- 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">${escapeHtmlText(PLACEHOLDER)}</div></foreignObject>`;
305
+ 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>`;
152
306
  }
153
- const body = escapeHtmlText(content);
307
+ const body = escapeHtmlText2(content);
154
308
  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>`;
155
309
  }
156
310
 
@@ -748,6 +902,9 @@ function rebuildItemSvg(item) {
748
902
  )
749
903
  };
750
904
  }
905
+ if (getLinkData(item)) {
906
+ return rebuildLinkItemSvg(item);
907
+ }
751
908
  if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
752
909
  const b = normalizeRect(item.bounds);
753
910
  return {
@@ -3710,8 +3867,77 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
3710
3867
  }
3711
3868
 
3712
3869
  // src/interaction/mutations.ts
3870
+ var LINK_CORNER_HANDLES = /* @__PURE__ */ new Set(["nw", "ne", "se", "sw"]);
3871
+ var clamp2 = (value, min, max) => Math.min(max, Math.max(min, value));
3872
+ var clampLinkResizeBounds = (startBounds, nextBounds, handle, intrinsicWidth) => {
3873
+ const next = normalizeRect(nextBounds);
3874
+ const scale = clamp2(
3875
+ next.width / Math.max(1e-9, intrinsicWidth),
3876
+ LINK_CARD_MIN_SCALE,
3877
+ LINK_CARD_MAX_SCALE
3878
+ );
3879
+ const width = intrinsicWidth * scale;
3880
+ const height = width / LINK_CARD_ASPECT;
3881
+ const start = normalizeRect(startBounds);
3882
+ const x0 = start.x;
3883
+ const y0 = start.y;
3884
+ const x1 = start.x + start.width;
3885
+ const y1 = start.y + start.height;
3886
+ switch (handle) {
3887
+ case "nw":
3888
+ return { x: x1 - width, y: y1 - height, width, height };
3889
+ case "ne":
3890
+ return { x: x0, y: y1 - height, width, height };
3891
+ case "sw":
3892
+ return { x: x1 - width, y: y0, width, height };
3893
+ default:
3894
+ return { x: x0, y: y0, width, height };
3895
+ }
3896
+ };
3713
3897
  function computeNewBoundsForResize(item, sb, handle, currentWorld) {
3714
3898
  const rot = getItemRotationRad(item);
3899
+ const link = getLinkData(item);
3900
+ if (link && item.customIntrinsicSize) {
3901
+ if (!LINK_CORNER_HANDLES.has(handle)) return sb;
3902
+ const intrinsicWidth = Math.max(1e-9, item.customIntrinsicSize.width);
3903
+ if (Math.abs(rot) < 1e-12) {
3904
+ const next = computeResizeBoundsFixedAspect(
3905
+ sb,
3906
+ handle,
3907
+ currentWorld,
3908
+ LINK_CARD_ASPECT
3909
+ );
3910
+ return clampLinkResizeBounds(sb, next, handle, intrinsicWidth);
3911
+ }
3912
+ const local2 = worldToItemLocal(
3913
+ currentWorld.x,
3914
+ currentWorld.y,
3915
+ sb.x,
3916
+ sb.y,
3917
+ sb.width,
3918
+ sb.height,
3919
+ rot
3920
+ );
3921
+ const localBounds3 = { x: 0, y: 0, width: sb.width, height: sb.height };
3922
+ const nextLocal = computeResizeBoundsFixedAspect(
3923
+ localBounds3,
3924
+ handle,
3925
+ local2,
3926
+ LINK_CARD_ASPECT
3927
+ );
3928
+ const clamped = clampLinkResizeBounds(
3929
+ localBounds3,
3930
+ nextLocal,
3931
+ handle,
3932
+ intrinsicWidth
3933
+ );
3934
+ return {
3935
+ x: sb.x + clamped.x,
3936
+ y: sb.y + clamped.y,
3937
+ width: clamped.width,
3938
+ height: clamped.height
3939
+ };
3940
+ }
3715
3941
  if (Math.abs(rot) < 1e-12) {
3716
3942
  if (item.toolKind === "image") {
3717
3943
  let aspect;