@ngenux/ngage-whiteboarding 1.0.3 → 1.0.5
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/README.md +0 -94
- package/dist/index.esm.js +102 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +102 -9
- package/dist/index.js.map +1 -1
- package/dist/src/components/Whiteboard/Board.d.ts +1 -0
- package/dist/src/components/Whiteboard/Board.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/Toolbar.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
- package/dist/src/utils/video-coordinates.d.ts +36 -0
- package/dist/src/utils/video-coordinates.d.ts.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,100 +190,6 @@ function CollaborativeApp() {
|
|
|
190
190
|
}
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
### Transparent Background Configuration
|
|
194
|
-
|
|
195
|
-
If you want to enforce a transparent background and prevent users from changing it:
|
|
196
|
-
|
|
197
|
-
```tsx
|
|
198
|
-
import { WhiteboardProvider, Whiteboard } from '@ngenux/ngage-whiteboarding';
|
|
199
|
-
|
|
200
|
-
function TransparentWhiteboard() {
|
|
201
|
-
return (
|
|
202
|
-
<WhiteboardProvider webSocketUrl="ws://localhost:3001">
|
|
203
|
-
<Whiteboard
|
|
204
|
-
roomId="transparent-room"
|
|
205
|
-
userId="user-123"
|
|
206
|
-
isAdmin={true}
|
|
207
|
-
allowedUsers={['user-123']}
|
|
208
|
-
transparentBackground={true} // Forces transparent & locks background
|
|
209
|
-
/>
|
|
210
|
-
</WhiteboardProvider>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
**Behavior:**
|
|
216
|
-
- When `transparentBackground={true}`:
|
|
217
|
-
- ✅ Background is automatically set to transparent
|
|
218
|
-
- 🔒 Background color picker is disabled and shows "Transparent background locked"
|
|
219
|
-
- Perfect for overlaying the whiteboard on existing content
|
|
220
|
-
|
|
221
|
-
- When `transparentBackground={false}` (default):
|
|
222
|
-
- ✅ Background defaults to white
|
|
223
|
-
- 🎨 Users can change background color freely
|
|
224
|
-
|
|
225
|
-
### Video Background for Annotation
|
|
226
|
-
|
|
227
|
-
Annotate over live video streams (screen-share, webcam, etc.):
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
import { WhiteboardProvider, Whiteboard } from '@ngenux/ngage-whiteboarding';
|
|
231
|
-
import { useEffect, useState } from 'react';
|
|
232
|
-
|
|
233
|
-
function VideoAnnotationWhiteboard() {
|
|
234
|
-
const [videoStream, setVideoStream] = useState<MediaStream | undefined>();
|
|
235
|
-
|
|
236
|
-
// Get screen share or webcam stream
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
async function getStream() {
|
|
239
|
-
try {
|
|
240
|
-
// Screen share example
|
|
241
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
242
|
-
video: { width: 1920, height: 1080 }
|
|
243
|
-
});
|
|
244
|
-
setVideoStream(stream);
|
|
245
|
-
} catch (err) {
|
|
246
|
-
console.error('Failed to get video stream:', err);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
getStream();
|
|
250
|
-
|
|
251
|
-
return () => {
|
|
252
|
-
// Cleanup: stop all tracks when component unmounts
|
|
253
|
-
if (videoStream) {
|
|
254
|
-
videoStream.getTracks().forEach(track => track.stop());
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
}, []);
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<WhiteboardProvider webSocketUrl="ws://localhost:3001">
|
|
261
|
-
<Whiteboard
|
|
262
|
-
roomId="video-annotation-room"
|
|
263
|
-
userId="user-123"
|
|
264
|
-
isAdmin={true}
|
|
265
|
-
allowedUsers={['user-123']}
|
|
266
|
-
videoStream={videoStream} // Video renders as background layer
|
|
267
|
-
/>
|
|
268
|
-
</WhiteboardProvider>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Features:**
|
|
274
|
-
- 🎥 **Video as background**: Stream renders behind whiteboard canvas
|
|
275
|
-
- 🖊️ **Draw over video**: All drawing tools work normally over the video
|
|
276
|
-
- 🔒 **Auto-transparent**: Background automatically set to transparent
|
|
277
|
-
- ⚡ **60fps performance**: HTML video layer, no canvas copying
|
|
278
|
-
- 📐 **Auto-scaling**: Video maintains aspect ratio with letterbox
|
|
279
|
-
- 🎯 **Pointer-events handled**: Video doesn't interfere with drawing
|
|
280
|
-
|
|
281
|
-
**Use cases:**
|
|
282
|
-
- Screen-share annotation for remote presentations
|
|
283
|
-
- Video call annotation
|
|
284
|
-
- Educational content markup
|
|
285
|
-
- Live streaming annotation
|
|
286
|
-
|
|
287
193
|
## 📝 API Reference
|
|
288
194
|
|
|
289
195
|
### WhiteboardProvider Props
|
package/dist/index.esm.js
CHANGED
|
@@ -36710,9 +36710,27 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36710
36710
|
const activeShapes = Object.values(state.activeDrawings);
|
|
36711
36711
|
return activeShapes.map(shape => (jsx(MemoizedShapeComponent, { shape: shape }, `active-${shape.id}`)));
|
|
36712
36712
|
}, [state.activeDrawings]);
|
|
36713
|
+
const getStageDataURL = useCallback((format = 'png') => {
|
|
36714
|
+
if (!stageRef.current) {
|
|
36715
|
+
return '';
|
|
36716
|
+
}
|
|
36717
|
+
try {
|
|
36718
|
+
const stage = stageRef.current;
|
|
36719
|
+
return stage.toDataURL({
|
|
36720
|
+
mimeType: format === 'png' ? 'image/png' : 'image/jpeg',
|
|
36721
|
+
quality: 1,
|
|
36722
|
+
pixelRatio: 2,
|
|
36723
|
+
});
|
|
36724
|
+
}
|
|
36725
|
+
catch (error) {
|
|
36726
|
+
console.error('Failed to get stage data URL:', error);
|
|
36727
|
+
return '';
|
|
36728
|
+
}
|
|
36729
|
+
}, []);
|
|
36713
36730
|
useImperativeHandle(ref, () => ({
|
|
36714
36731
|
exportAsImage,
|
|
36715
36732
|
exportAsPDF,
|
|
36733
|
+
getStageDataURL,
|
|
36716
36734
|
}));
|
|
36717
36735
|
// Memoized cursor style for performance
|
|
36718
36736
|
const cursorStyle = useMemo(() => ({
|
|
@@ -37033,6 +37051,14 @@ const Undo2 = createLucideIcon("Undo2", [
|
|
|
37033
37051
|
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = false, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
|
|
37034
37052
|
const { state, dispatch } = useWhiteboard();
|
|
37035
37053
|
const [isVisible, setIsVisible] = useState(shouldBeOpenByDefault);
|
|
37054
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
37055
|
+
// Wait for context to be fully initialized before rendering
|
|
37056
|
+
// Check that state.userId is set to ensure context is fully ready
|
|
37057
|
+
useEffect(() => {
|
|
37058
|
+
if (state && state.userId) {
|
|
37059
|
+
setIsInitialized(true);
|
|
37060
|
+
}
|
|
37061
|
+
}, [state]);
|
|
37036
37062
|
const handleToggleVisibility = () => {
|
|
37037
37063
|
setIsVisible(!isVisible);
|
|
37038
37064
|
};
|
|
@@ -37094,6 +37120,10 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
|
|
|
37094
37120
|
}
|
|
37095
37121
|
}
|
|
37096
37122
|
};
|
|
37123
|
+
// Don't render until fully initialized to prevent race conditions
|
|
37124
|
+
if (!isInitialized) {
|
|
37125
|
+
return null;
|
|
37126
|
+
}
|
|
37097
37127
|
return (jsxs(Fragment, { children: [jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsx("button", { className: "w-10 h-10 flex items-center justify-center bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300", onClick: handleToggleVisibility, title: "Show Tools", children: jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxs("div", { className: "flex items-center gap-1 p-1", children: [isAdmin && (jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${isGloballyUnlocked
|
|
37098
37128
|
? 'bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-900/70'
|
|
37099
37129
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'}`, onClick: handleLockToggle, title: isGloballyUnlocked ? 'Whiteboard unlocked for all users - Click to lock' : 'Whiteboard locked - Click to unlock for all users', children: isGloballyUnlocked ? jsx(LockOpen, { size: 16, className: "text-current" }) : jsx(Lock, { size: 16, className: "text-current" }) })), jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleUndo, disabled: !hasToolAccess, title: hasToolAccess ? 'Undo' : 'Access restricted', children: jsx(Undo2, { size: 16, className: "text-current" }) }), jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded ${hasToolAccess ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300' : 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'}`, onClick: handleRedo, disabled: !hasToolAccess, title: hasToolAccess ? 'Redo' : 'Access restricted', children: jsx(Redo2, { size: 16, className: "text-current" }) }), jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
@@ -37111,6 +37141,14 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37111
37141
|
const [position, setPosition] = useState({ x: 20, y: 80 });
|
|
37112
37142
|
const [hasEverHadAccess, setHasEverHadAccess] = useState(hasToolAccess);
|
|
37113
37143
|
const [wasManuallyClosedAfterAccess, setWasManuallyClosedAfterAccess] = useState(false);
|
|
37144
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
37145
|
+
// Wait for context to be fully initialized before rendering
|
|
37146
|
+
// Check that state.userId is set to ensure context is fully ready
|
|
37147
|
+
useEffect(() => {
|
|
37148
|
+
if (state && state.userId) {
|
|
37149
|
+
setIsInitialized(true);
|
|
37150
|
+
}
|
|
37151
|
+
}, [state]);
|
|
37114
37152
|
// Set white as default stroke color when video background is active
|
|
37115
37153
|
useEffect(() => {
|
|
37116
37154
|
if (hasVideoBackground && state.color === '#000000') {
|
|
@@ -37231,6 +37269,10 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37231
37269
|
// Only update local state - don't broadcast opacity changes to other users
|
|
37232
37270
|
dispatch({ type: 'SET_OPACITY', payload: opacity });
|
|
37233
37271
|
};
|
|
37272
|
+
// Don't render until fully initialized to prevent race conditions
|
|
37273
|
+
if (!isInitialized) {
|
|
37274
|
+
return null;
|
|
37275
|
+
}
|
|
37234
37276
|
if (!isVisible) {
|
|
37235
37277
|
// Only show the sidebar toggle button if user has tool access
|
|
37236
37278
|
if (!hasToolAccess) {
|
|
@@ -37244,16 +37286,20 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37244
37286
|
width: '250px',
|
|
37245
37287
|
maxHeight: '80vh',
|
|
37246
37288
|
cursor: isDragging ? 'grabbing' : 'grab'
|
|
37247
|
-
}, onMouseDown: handleMouseDown, children: [jsxs("div", { className: "flex items-center justify-between p-2 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 rounded-t-lg", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsx("span", { className: "text-xs bg-orange-100 dark:bg-orange-900 text-orange-600 dark:text-orange-300 px-2 py-1 rounded", children: "Locked" }))] }), jsx("button", { className: "close-button p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded", onClick: handleManualClose, onMouseDown: (e) => e.stopPropagation(), title: "Close Panel", children: jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxs("div", { className: "mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg text-center", children: [jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded
|
|
37248
|
-
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37289
|
+
}, onMouseDown: handleMouseDown, children: [jsxs("div", { className: "flex items-center justify-between p-2 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 rounded-t-lg", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsx("span", { className: "text-xs bg-orange-100 dark:bg-orange-900 text-orange-600 dark:text-orange-300 px-2 py-1 rounded", children: "Locked" }))] }), jsx("button", { className: "close-button p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded", onClick: handleManualClose, onMouseDown: (e) => e.stopPropagation(), title: "Close Panel", children: jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxs("div", { className: "mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg text-center", children: [jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded transition-all relative ${!hasToolAccess
|
|
37290
|
+
? 'opacity-50 cursor-not-allowed border-2 border-gray-200 dark:border-gray-700'
|
|
37249
37291
|
: state.color === color
|
|
37250
|
-
? 'border-
|
|
37292
|
+
? 'border-[3px] border-yellow-400 dark:border-yellow-300 scale-125 shadow-lg'
|
|
37251
37293
|
: color === '#FFFFFF'
|
|
37252
|
-
? 'border-gray-
|
|
37253
|
-
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37294
|
+
? 'border-2 border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
|
37295
|
+
: 'border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37254
37296
|
backgroundColor: color,
|
|
37255
|
-
boxShadow: color === '#FFFFFF'
|
|
37256
|
-
|
|
37297
|
+
boxShadow: color === '#FFFFFF'
|
|
37298
|
+
? 'inset 0 0 0 1px rgba(0,0,0,0.15)'
|
|
37299
|
+
: state.color === color
|
|
37300
|
+
? '0 4px 12px rgba(234, 179, 8, 0.5), 0 0 0 2px rgba(234, 179, 8, 0.3)'
|
|
37301
|
+
: 'none'
|
|
37302
|
+
}, onClick: () => handleColorChange(color), disabled: !hasToolAccess, title: hasToolAccess ? (color === '#FFFFFF' ? 'White' : color) : 'Access restricted', children: state.color === color && (jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: color === '#FFFFFF' || color === '#EA580C' || color === '#DC2626' ? '#000000' : '#FFFFFF', strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }) })) }, color))) })] }), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Background" }), disableBackgroundChange && (jsx("div", { className: "p-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg text-center", children: jsx("div", { className: "text-blue-700 dark:text-blue-300 text-xs", title: "Disabled when video background is active", children: "\uD83D\uDD12 Background selection locked" }) })), !disableBackgroundChange && (jsx("div", { className: "grid grid-cols-6 gap-1", children: backgroundColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded border-2 transition-all relative ${!hasToolAccess
|
|
37257
37303
|
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37258
37304
|
: state.backgroundColor === color
|
|
37259
37305
|
? 'border-blue-400 dark:border-blue-500 scale-110'
|
|
@@ -42435,10 +42481,57 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42435
42481
|
}, [state.activeDrawings, state.lastClearTimestamp, dispatch]);
|
|
42436
42482
|
// Export functions to pass to toolbar
|
|
42437
42483
|
const handleExportImage = useCallback((format) => {
|
|
42438
|
-
if (boardRef.current) {
|
|
42484
|
+
if (!boardRef.current) {
|
|
42485
|
+
return;
|
|
42486
|
+
}
|
|
42487
|
+
// If video is active, capture both video and canvas
|
|
42488
|
+
if (videoStream && videoRef.current) {
|
|
42489
|
+
const video = videoRef.current;
|
|
42490
|
+
const canvas = document.createElement('canvas');
|
|
42491
|
+
const ctx = canvas.getContext('2d');
|
|
42492
|
+
if (!ctx)
|
|
42493
|
+
return;
|
|
42494
|
+
// Set canvas size to match the whiteboard
|
|
42495
|
+
canvas.width = video.videoWidth || video.offsetWidth;
|
|
42496
|
+
canvas.height = video.videoHeight || video.offsetHeight;
|
|
42497
|
+
try {
|
|
42498
|
+
// Draw the video frame
|
|
42499
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
42500
|
+
// Get the Konva stage data URL
|
|
42501
|
+
const stageDataURL = boardRef.current.getStageDataURL(format);
|
|
42502
|
+
// Create an image from the stage
|
|
42503
|
+
const stageImage = new Image();
|
|
42504
|
+
stageImage.onload = () => {
|
|
42505
|
+
// Draw the annotations on top of the video
|
|
42506
|
+
ctx.drawImage(stageImage, 0, 0, canvas.width, canvas.height);
|
|
42507
|
+
// Convert to blob and download
|
|
42508
|
+
canvas.toBlob((blob) => {
|
|
42509
|
+
if (blob) {
|
|
42510
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace('T', '_').replace(/:/g, '-');
|
|
42511
|
+
const filename = `whiteboard_video_annotation_${timestamp}.${format}`;
|
|
42512
|
+
const link = document.createElement('a');
|
|
42513
|
+
link.download = filename;
|
|
42514
|
+
link.href = URL.createObjectURL(blob);
|
|
42515
|
+
document.body.appendChild(link);
|
|
42516
|
+
link.click();
|
|
42517
|
+
document.body.removeChild(link);
|
|
42518
|
+
URL.revokeObjectURL(link.href);
|
|
42519
|
+
}
|
|
42520
|
+
}, format === 'png' ? 'image/png' : 'image/jpeg', 1.0);
|
|
42521
|
+
};
|
|
42522
|
+
stageImage.src = stageDataURL;
|
|
42523
|
+
}
|
|
42524
|
+
catch (error) {
|
|
42525
|
+
console.error('Failed to export video with annotations:', error);
|
|
42526
|
+
// Fallback to regular export
|
|
42527
|
+
boardRef.current.exportAsImage(format);
|
|
42528
|
+
}
|
|
42529
|
+
}
|
|
42530
|
+
else {
|
|
42531
|
+
// No video - regular export
|
|
42439
42532
|
boardRef.current.exportAsImage(format);
|
|
42440
42533
|
}
|
|
42441
|
-
}, []);
|
|
42534
|
+
}, [videoStream]);
|
|
42442
42535
|
const handleExportPDF = useCallback(() => {
|
|
42443
42536
|
if (boardRef.current) {
|
|
42444
42537
|
boardRef.current.exportAsPDF();
|