@widgetic/canvas 0.5.6 → 0.5.9

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.
@@ -122,9 +122,8 @@
122
122
  $: isFrame = customType === 'frame' || objectType === 'frame';
123
123
  // WidgetShape is a Rect subclass with _customType === 'image'
124
124
  // isImagePlaceholder: when no image loaded yet (_hasImage is false)
125
- $: isImagePlaceholder = customType === 'image' && !selectedObject?._hasImage;
126
- // isRect excludes WidgetShape (which has _customType === 'image')
127
- $: isRect = objectType === 'rect' && !isFrame && customType !== 'image';
125
+ $: isImagePlaceholder = (customType === 'image' || customType === 'video') && !selectedObject?._hasImage;
126
+ $: isRect = objectType === 'rect' && !isFrame && customType !== 'image' && customType !== 'video';
128
127
  $: isTextbox = objectType === 'textbox';
129
128
  $: isPath = objectType === 'path' && !customType;
130
129
  $: isArrow = objectType === 'path' && customType === 'arrow';
@@ -141,7 +140,7 @@
141
140
  $: isMultiSelect = (isGroup && !isFrame) || isActiveSelection;
142
141
  $: isPlainGroup = isGroup && !isFrame;
143
142
  $: isLine = objectType === 'line';
144
- $: isImage = objectType === 'image' || customType === 'image';
143
+ $: isImage = objectType === 'image' || customType === 'image' || customType === 'video';
145
144
  // Widget link: true when the selected object has already been converted to a widget
146
145
  $: hasWidgetId = !!selectedObject?._widgetId;
147
146
  $: widgetId = selectedObject?._widgetId ?? null;
@@ -195,6 +194,7 @@
195
194
  if (obj._customType === 'frame') return 'Frame';
196
195
  if (obj._customType === 'arrow') return 'Arrow';
197
196
  if (obj._customType === 'triangle') return 'Triangle';
197
+ if (obj._customType === 'video') return 'Video';
198
198
  if (obj._customType === 'image' || obj.type === 'image') return 'Image';
199
199
  // Library shapes: derive a readable name from _shapeKey (e.g. 'arrow-right' → 'Arrow Right')
200
200
  // Fall back to 'Custom Shape (Drawing)' if _shapeKey is missing (old saves).
@@ -226,7 +226,13 @@
226
226
  // Get current values from selected object
227
227
  // For Frames (Group-based), read fill/stroke from custom properties (_frameFill, _frameStroke)
228
228
  // because the actual fill/stroke is on the background Rect child, not the Group itself
229
- $: currentFill = isFrame ? (selectedObject?._frameFill ?? '#FFFFFF') : (selectedObject?.fill ?? '#FFFFFF');
229
+ $: usesPathBackgroundFill = selectedObject?._cornerRadiusMode === 'strokeRound'
230
+ || ['cross', 'plus', 'minus', 'check'].includes(selectedObject?._shapeKey);
231
+ $: currentFill = isFrame
232
+ ? (selectedObject?._frameFill ?? '#FFFFFF')
233
+ : (usesPathBackgroundFill
234
+ ? (selectedObject?._iconFill || selectedObject?.backgroundColor || '#FFFFFF')
235
+ : (selectedObject?.fill ?? '#FFFFFF'));
230
236
  $: currentFillOpacity = Math.round((selectedObject?._fillOpacity ?? 1) * 100);
231
237
  $: currentStroke = isFrame ? (selectedObject?._frameStroke ?? '#000000') : (selectedObject?.stroke ?? '#000000');
232
238
  $: currentStrokeWidth = isFrame ? (selectedObject?._frameStrokeWidth ?? 1) : (selectedObject?.strokeWidth ?? 1);
@@ -568,7 +574,7 @@
568
574
  <span class="prop-label">Convert to Widget</span>
569
575
  <div class="capture-section">
570
576
  <button class="convert-widget-btn" onclick={onConvertToWidget}>
571
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
577
+ <svg class="convert-widget-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
572
578
  <rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
573
579
  <line x1="7" y1="2" x2="7" y2="22"/>
574
580
  <line x1="17" y1="2" x2="17" y2="22"/>
@@ -1305,6 +1311,16 @@
1305
1311
  border-radius: 8px;
1306
1312
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1307
1313
  }
1314
+
1315
+ .props-panel-el :global(button:focus),
1316
+ .props-panel-el :global(button:focus-visible),
1317
+ .props-panel-el :global(a:focus),
1318
+ .props-panel-el :global(a:focus-visible),
1319
+ .props-panel-el :global(input:focus),
1320
+ .props-panel-el :global(input:focus-visible) {
1321
+ outline: none !important;
1322
+ box-shadow: none !important;
1323
+ }
1308
1324
 
1309
1325
  /* Sticky header: stays at top, never scrolls */
1310
1326
  .props-panel-sticky-header {
@@ -1550,9 +1566,14 @@
1550
1566
  box-shadow: 0 2px 4px rgba(49, 79, 47, 0.3);
1551
1567
  }
1552
1568
 
1553
- .convert-widget-btn svg {
1569
+ /* Host apps (site / DS) leak global svg fill/stroke; keep this icon white on the green button. */
1570
+ .convert-widget-btn :global(svg),
1571
+ .convert-widget-btn :global(svg *) {
1554
1572
  width: 16px;
1555
1573
  height: 16px;
1574
+ color: #ffffff !important;
1575
+ fill: none !important;
1576
+ stroke: currentColor !important;
1556
1577
  }
1557
1578
 
1558
1579
  .widget-linked-section {
@@ -1669,7 +1690,9 @@
1669
1690
  border-radius: 4px;
1670
1691
  cursor: pointer;
1671
1692
  transition: all 0.15s;
1672
- width: fit-content;
1693
+ width: 100%;
1694
+ box-sizing: border-box;
1695
+ justify-content: center;
1673
1696
  }
1674
1697
  .refresh-screenshot-btn:hover {
1675
1698
  color: #166534;
@@ -1695,14 +1718,18 @@
1695
1718
  border: 1px solid #fecaca;
1696
1719
  border-radius: 4px;
1697
1720
  cursor: pointer;
1698
- width: fit-content;
1721
+ width: 100%;
1722
+ box-sizing: border-box;
1723
+ justify-content: center;
1699
1724
  }
1700
1725
  .set-primary-btn:hover {
1701
1726
  background: #fee2e2;
1702
1727
  border-color: #f87171;
1703
1728
  }
1704
1729
  .primary-widget-badge {
1705
- display: inline-block;
1730
+ display: flex;
1731
+ align-items: center;
1732
+ justify-content: center;
1706
1733
  margin-top: 4px;
1707
1734
  padding: 3px 8px;
1708
1735
  font-size: 11px;
@@ -1711,7 +1738,9 @@
1711
1738
  background: #fef2f2;
1712
1739
  border: 1px solid #fecaca;
1713
1740
  border-radius: 4px;
1714
- width: fit-content;
1741
+ width: 100%;
1742
+ box-sizing: border-box;
1743
+ text-align: center;
1715
1744
  }
1716
1745
 
1717
1746
  .widget-orphan-notice {
@@ -24,8 +24,17 @@ export declare class ImageShape extends Rect {
24
24
  _cornerRoundness: number;
25
25
  _cornerRoundnessPixels: number;
26
26
  _objectId: string | null;
27
+ _videoElement: HTMLVideoElement | null;
28
+ _videoObjectUrl: string | null;
27
29
  _render(ctx: CanvasRenderingContext2D): void;
28
30
  private _renderImage;
31
+ getVideoPlayBadgeRadius(): number;
32
+ isVideoPlayBadgeHit(scenePoint: {
33
+ x: number;
34
+ y: number;
35
+ }): boolean;
36
+ toggleVideoPlayback(): Promise<boolean>;
37
+ private _renderVideoChrome;
29
38
  private _renderPlaceholderOverlay;
30
39
  toObject(propertiesToInclude?: any[]): any;
31
40
  static fromObject(object: Record<string, unknown>, options?: Record<string, unknown>): Promise<ImageShape>;
@@ -36,9 +36,12 @@ export class ImageShape extends Rect {
36
36
  _cornerRoundness = 0;
37
37
  _cornerRoundnessPixels = 0;
38
38
  _objectId = null;
39
+ _videoElement = null;
40
+ _videoObjectUrl = null;
39
41
  // ── Placeholder rendering ─────────────────────────────────────────────────
40
42
  _render(ctx) {
41
- const hasImage = this._hasImage && this._imageElement;
43
+ const hasVideoFrame = this._customType === 'video' && !!this._videoElement && this._videoElement.readyState >= 2;
44
+ const hasImage = (this._hasImage && this._imageElement) || hasVideoFrame;
42
45
  if (hasImage) {
43
46
  this._renderImage(ctx);
44
47
  }
@@ -78,9 +81,12 @@ export class ImageShape extends Rect {
78
81
  ctx.closePath();
79
82
  ctx.clip();
80
83
  const img = this._imageElement;
81
- const imgReady = img.complete && img.naturalWidth > 0;
82
- if (imgReady) {
83
- if (this._isWidgetImage) {
84
+ const video = this._customType === 'video' ? this._videoElement : null;
85
+ const useVideo = !!(video && video.readyState >= 2);
86
+ const source = useVideo ? video : (img && img.complete && img.naturalWidth > 0 ? img : null);
87
+ const imgReady = !!source;
88
+ if (source) {
89
+ if (this._isWidgetImage && img && img.complete && img.naturalWidth > 0 && !useVideo) {
84
90
  const imgW = img.naturalWidth;
85
91
  const imgH = img.naturalHeight;
86
92
  const scale = Math.min(w / imgW, h / imgH);
@@ -89,10 +95,13 @@ export class ImageShape extends Rect {
89
95
  ctx.drawImage(img, -dw / 2, -dh / 2, dw, dh);
90
96
  }
91
97
  else {
92
- ctx.drawImage(img, -w / 2, -h / 2, w, h);
98
+ ctx.drawImage(source, -w / 2, -h / 2, w, h);
93
99
  }
94
100
  }
95
101
  ctx.restore();
102
+ if (this._customType === 'video' && imgReady) {
103
+ this._renderVideoChrome(ctx, w, h);
104
+ }
96
105
  if (this.stroke && (this.strokeWidth ?? 0) > 0) {
97
106
  ctx.save();
98
107
  ctx.strokeStyle = this.stroke;
@@ -126,32 +135,114 @@ export class ImageShape extends Rect {
126
135
  ctx.restore();
127
136
  }
128
137
  }
138
+ getVideoPlayBadgeRadius() {
139
+ const w = this.width || 100;
140
+ const h = this.height || 100;
141
+ const size = Math.max(18, Math.min(w, h) * 0.22);
142
+ return (size / 2) * Math.max(this.scaleX || 1, this.scaleY || 1);
143
+ }
144
+ isVideoPlayBadgeHit(scenePoint) {
145
+ if (this._customType !== 'video' || !this._videoElement)
146
+ return false;
147
+ const center = this.getCenterPoint();
148
+ const radius = this.getVideoPlayBadgeRadius();
149
+ const dx = scenePoint.x - center.x;
150
+ const dy = scenePoint.y - center.y;
151
+ return dx * dx + dy * dy <= radius * radius;
152
+ }
153
+ async toggleVideoPlayback() {
154
+ const video = this._videoElement;
155
+ if (!video) {
156
+ canvasWarn('ImageShape: no video element to play');
157
+ return false;
158
+ }
159
+ try {
160
+ if (video.paused) {
161
+ await video.play();
162
+ canvasLog('ImageShape: video playing');
163
+ }
164
+ else {
165
+ video.pause();
166
+ canvasLog('ImageShape: video paused');
167
+ }
168
+ this.dirty = true;
169
+ return true;
170
+ }
171
+ catch (err) {
172
+ canvasWarn('ImageShape: video play/pause failed', err);
173
+ return false;
174
+ }
175
+ }
176
+ _renderVideoChrome(ctx, w, h) {
177
+ const video = this._videoElement;
178
+ const playing = !!(video && !video.paused && !video.ended);
179
+ const size = Math.max(18, Math.min(w, h) * 0.22);
180
+ ctx.save();
181
+ ctx.beginPath();
182
+ ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
183
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
184
+ ctx.fill();
185
+ ctx.fillStyle = '#ffffff';
186
+ if (playing) {
187
+ const barW = size * 0.08;
188
+ const barH = size * 0.28;
189
+ const gap = size * 0.08;
190
+ ctx.fillRect(-gap / 2 - barW, -barH / 2, barW, barH);
191
+ ctx.fillRect(gap / 2, -barH / 2, barW, barH);
192
+ }
193
+ else {
194
+ ctx.beginPath();
195
+ const tri = size * 0.18;
196
+ ctx.moveTo(-tri * 0.35, -tri);
197
+ ctx.lineTo(tri * 0.85, 0);
198
+ ctx.lineTo(-tri * 0.35, tri);
199
+ ctx.closePath();
200
+ ctx.fill();
201
+ }
202
+ if (video && Number.isFinite(video.duration) && video.duration > 0) {
203
+ const barH = Math.max(4, Math.min(8, h * 0.06));
204
+ const progress = Math.max(0, Math.min(1, video.currentTime / video.duration));
205
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
206
+ ctx.fillRect(-w / 2, h / 2 - barH, w, barH);
207
+ ctx.fillStyle = '#ffffff';
208
+ ctx.fillRect(-w / 2, h / 2 - barH, w * progress, barH);
209
+ }
210
+ ctx.restore();
211
+ }
129
212
  // ── Placeholder icon + text overlay ──────────────────────────────────────
130
213
  _renderPlaceholderOverlay(ctx) {
131
- const availableW = this.width || 100;
132
- const availableH = this.height || 100;
133
- const contentHeight = 76;
134
- const contentScale = Math.min(1, availableH / contentHeight, availableW / 100);
214
+ const scaleX = Math.abs(this.scaleX || 1) || 1;
215
+ const scaleY = Math.abs(this.scaleY || 1) || 1;
216
+ ctx.save();
217
+ ctx.scale(1 / scaleX, 1 / scaleY);
218
+ const visualW = Math.abs((this.width || 100) * scaleX);
219
+ const visualH = Math.abs((this.height || 100) * scaleY);
220
+ const iconSize = 36;
221
+ const fontSize = 16;
222
+ const lineGap = fontSize + 6;
223
+ const contentWidth = 168;
224
+ const contentHeight = iconSize + 12 + lineGap * 2;
225
+ const contentScale = Math.max(0.7, Math.min((visualW * 0.88) / contentWidth, (visualH * 0.7) / contentHeight));
135
226
  ctx.fillStyle = '#94a3b8';
136
227
  ctx.textAlign = 'center';
137
228
  ctx.textBaseline = 'middle';
138
- if (contentScale < 0.3) {
139
- const tinyIconSize = Math.max(8, 24 * contentScale);
140
- ctx.font = `${tinyIconSize}px Arial`;
141
- ctx.fillText('🖼️', 0, 0);
229
+ ctx.save();
230
+ ctx.scale(contentScale, contentScale);
231
+ ctx.font = `${iconSize}px Arial`;
232
+ ctx.fillText(this._customType === 'video' ? '🎬' : '🖼️', 0, -28);
233
+ ctx.font = `${fontSize}px Arial`;
234
+ if (this._customType === 'video') {
235
+ ctx.fillText('Drop video', 0, 8);
236
+ ctx.fillText('or', 0, 8 + lineGap);
237
+ ctx.fillText('double click', 0, 8 + lineGap * 2);
142
238
  }
143
239
  else {
144
- ctx.save();
145
- ctx.scale(contentScale, contentScale);
146
- ctx.font = '24px Arial';
147
- ctx.fillText('🖼️', 0, -28);
148
- const fontSize = 12;
149
- ctx.font = `${fontSize}px Arial`;
150
- ctx.fillText('Drop image', 0, 0);
151
- ctx.fillText('or', 0, fontSize + 4);
152
- ctx.fillText('double click', 0, (fontSize + 4) * 2);
153
- ctx.restore();
240
+ ctx.fillText('Drop image', 0, 8);
241
+ ctx.fillText('or', 0, 8 + lineGap);
242
+ ctx.fillText('double click', 0, 8 + lineGap * 2);
154
243
  }
244
+ ctx.restore();
245
+ ctx.restore();
155
246
  }
156
247
  // ── Serialisation ─────────────────────────────────────────────────────────
157
248
  toObject(propertiesToInclude) {
@@ -225,7 +316,7 @@ classRegistry.setClass(ImageShape);
225
316
  * swapping the prototype chain. No object re-creation needed.
226
317
  */
227
318
  export function upgradeToImageShape(obj) {
228
- if (!obj || obj._customType !== 'image')
319
+ if (!obj || (obj._customType !== 'image' && obj._customType !== 'video'))
229
320
  return null;
230
321
  if (obj instanceof ImageShape)
231
322
  return obj;
@@ -25,6 +25,14 @@
25
25
 
26
26
  let triggerEl: HTMLElement | null = null;
27
27
 
28
+ $: if (text) refreshVisibleTooltip();
29
+
30
+ function refreshVisibleTooltip() {
31
+ if (tooltipBodyEl && tooltipBodyEl.style.display === 'block') {
32
+ show();
33
+ }
34
+ }
35
+
28
36
  // Lazily-created body-level elements
29
37
  let tooltipBodyEl: HTMLElement | null = null;
30
38
  let arrowBodyEl: HTMLElement | null = null;
@@ -48,8 +56,8 @@
48
56
  lineHeight: '1.4',
49
57
  padding: '5px 10px',
50
58
  borderRadius: '6px',
51
- whiteSpace: 'normal',
52
- maxWidth: '200px',
59
+ whiteSpace: 'nowrap',
60
+ maxWidth: 'none',
53
61
  textAlign: 'center',
54
62
  boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
55
63
  userSelect: 'none',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/canvas",
3
- "version": "0.5.6",
3
+ "version": "0.5.9",
4
4
  "type": "module",
5
5
  "svelte": "./dist/index.js",
6
6
  "module": "./dist/index.js",