@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/dist/index.js
CHANGED
|
@@ -36730,9 +36730,27 @@ const BoardComponent = React.forwardRef(({ roomId = 'default-room', queueAction,
|
|
|
36730
36730
|
const activeShapes = Object.values(state.activeDrawings);
|
|
36731
36731
|
return activeShapes.map(shape => (jsxRuntime.jsx(MemoizedShapeComponent, { shape: shape }, `active-${shape.id}`)));
|
|
36732
36732
|
}, [state.activeDrawings]);
|
|
36733
|
+
const getStageDataURL = React.useCallback((format = 'png') => {
|
|
36734
|
+
if (!stageRef.current) {
|
|
36735
|
+
return '';
|
|
36736
|
+
}
|
|
36737
|
+
try {
|
|
36738
|
+
const stage = stageRef.current;
|
|
36739
|
+
return stage.toDataURL({
|
|
36740
|
+
mimeType: format === 'png' ? 'image/png' : 'image/jpeg',
|
|
36741
|
+
quality: 1,
|
|
36742
|
+
pixelRatio: 2,
|
|
36743
|
+
});
|
|
36744
|
+
}
|
|
36745
|
+
catch (error) {
|
|
36746
|
+
console.error('Failed to get stage data URL:', error);
|
|
36747
|
+
return '';
|
|
36748
|
+
}
|
|
36749
|
+
}, []);
|
|
36733
36750
|
React.useImperativeHandle(ref, () => ({
|
|
36734
36751
|
exportAsImage,
|
|
36735
36752
|
exportAsPDF,
|
|
36753
|
+
getStageDataURL,
|
|
36736
36754
|
}));
|
|
36737
36755
|
// Memoized cursor style for performance
|
|
36738
36756
|
const cursorStyle = React.useMemo(() => ({
|
|
@@ -37053,6 +37071,14 @@ const Undo2 = createLucideIcon("Undo2", [
|
|
|
37053
37071
|
const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = false, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
|
|
37054
37072
|
const { state, dispatch } = useWhiteboard();
|
|
37055
37073
|
const [isVisible, setIsVisible] = React.useState(shouldBeOpenByDefault);
|
|
37074
|
+
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
37075
|
+
// Wait for context to be fully initialized before rendering
|
|
37076
|
+
// Check that state.userId is set to ensure context is fully ready
|
|
37077
|
+
React.useEffect(() => {
|
|
37078
|
+
if (state && state.userId) {
|
|
37079
|
+
setIsInitialized(true);
|
|
37080
|
+
}
|
|
37081
|
+
}, [state]);
|
|
37056
37082
|
const handleToggleVisibility = () => {
|
|
37057
37083
|
setIsVisible(!isVisible);
|
|
37058
37084
|
};
|
|
@@ -37114,6 +37140,10 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
|
|
|
37114
37140
|
}
|
|
37115
37141
|
}
|
|
37116
37142
|
};
|
|
37143
|
+
// Don't render until fully initialized to prevent race conditions
|
|
37144
|
+
if (!isInitialized) {
|
|
37145
|
+
return null;
|
|
37146
|
+
}
|
|
37117
37147
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "absolute top-5 left-1/2 transform -translate-x-1/2 flex flex-col items-center z-10", children: [!isVisible && (jsxRuntime.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: jsxRuntime.jsx(ChevronDown, { size: 16, className: "text-current" }) })), isVisible && (jsxRuntime.jsx("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-1 p-1", children: [isAdmin && (jsxRuntime.jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${isGloballyUnlocked
|
|
37118
37148
|
? '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'
|
|
37119
37149
|
: '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 ? jsxRuntime.jsx(LockOpen, { size: 16, className: "text-current" }) : jsxRuntime.jsx(Lock, { size: 16, className: "text-current" }) })), jsxRuntime.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: jsxRuntime.jsx(Undo2, { size: 16, className: "text-current" }) }), jsxRuntime.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: jsxRuntime.jsx(Redo2, { size: 16, className: "text-current" }) }), jsxRuntime.jsx("div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), tools.map((tool) => (jsxRuntime.jsx("button", { className: `w-10 h-10 flex items-center justify-center rounded transition-colors ${!hasToolAccess
|
|
@@ -37131,6 +37161,14 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37131
37161
|
const [position, setPosition] = React.useState({ x: 20, y: 80 });
|
|
37132
37162
|
const [hasEverHadAccess, setHasEverHadAccess] = React.useState(hasToolAccess);
|
|
37133
37163
|
const [wasManuallyClosedAfterAccess, setWasManuallyClosedAfterAccess] = React.useState(false);
|
|
37164
|
+
const [isInitialized, setIsInitialized] = React.useState(false);
|
|
37165
|
+
// Wait for context to be fully initialized before rendering
|
|
37166
|
+
// Check that state.userId is set to ensure context is fully ready
|
|
37167
|
+
React.useEffect(() => {
|
|
37168
|
+
if (state && state.userId) {
|
|
37169
|
+
setIsInitialized(true);
|
|
37170
|
+
}
|
|
37171
|
+
}, [state]);
|
|
37134
37172
|
// Set white as default stroke color when video background is active
|
|
37135
37173
|
React.useEffect(() => {
|
|
37136
37174
|
if (hasVideoBackground && state.color === '#000000') {
|
|
@@ -37251,6 +37289,10 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37251
37289
|
// Only update local state - don't broadcast opacity changes to other users
|
|
37252
37290
|
dispatch({ type: 'SET_OPACITY', payload: opacity });
|
|
37253
37291
|
};
|
|
37292
|
+
// Don't render until fully initialized to prevent race conditions
|
|
37293
|
+
if (!isInitialized) {
|
|
37294
|
+
return null;
|
|
37295
|
+
}
|
|
37254
37296
|
if (!isVisible) {
|
|
37255
37297
|
// Only show the sidebar toggle button if user has tool access
|
|
37256
37298
|
if (!hasToolAccess) {
|
|
@@ -37264,16 +37306,20 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37264
37306
|
width: '250px',
|
|
37265
37307
|
maxHeight: '80vh',
|
|
37266
37308
|
cursor: isDragging ? 'grabbing' : 'grab'
|
|
37267
|
-
}, onMouseDown: handleMouseDown, children: [jsxRuntime.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: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsxRuntime.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" }))] }), jsxRuntime.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: jsxRuntime.jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxRuntime.jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxRuntime.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: [jsxRuntime.jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsxRuntime.jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxRuntime.jsxs("div", { className: "mb-4", children: [jsxRuntime.jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsxRuntime.jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsxRuntime.jsx("button", { className: `w-6 h-6 rounded
|
|
37268
|
-
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37309
|
+
}, onMouseDown: handleMouseDown, children: [jsxRuntime.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: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsxRuntime.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" }))] }), jsxRuntime.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: jsxRuntime.jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxRuntime.jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxRuntime.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: [jsxRuntime.jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsxRuntime.jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxRuntime.jsxs("div", { className: "mb-4", children: [jsxRuntime.jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsxRuntime.jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsxRuntime.jsx("button", { className: `w-6 h-6 rounded transition-all relative ${!hasToolAccess
|
|
37310
|
+
? 'opacity-50 cursor-not-allowed border-2 border-gray-200 dark:border-gray-700'
|
|
37269
37311
|
: state.color === color
|
|
37270
|
-
? 'border-
|
|
37312
|
+
? 'border-[3px] border-yellow-400 dark:border-yellow-300 scale-125 shadow-lg'
|
|
37271
37313
|
: color === '#FFFFFF'
|
|
37272
|
-
? 'border-gray-
|
|
37273
|
-
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37314
|
+
? 'border-2 border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
|
37315
|
+
: 'border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37274
37316
|
backgroundColor: color,
|
|
37275
|
-
boxShadow: color === '#FFFFFF'
|
|
37276
|
-
|
|
37317
|
+
boxShadow: color === '#FFFFFF'
|
|
37318
|
+
? 'inset 0 0 0 1px rgba(0,0,0,0.15)'
|
|
37319
|
+
: state.color === color
|
|
37320
|
+
? '0 4px 12px rgba(234, 179, 8, 0.5), 0 0 0 2px rgba(234, 179, 8, 0.3)'
|
|
37321
|
+
: 'none'
|
|
37322
|
+
}, onClick: () => handleColorChange(color), disabled: !hasToolAccess, title: hasToolAccess ? (color === '#FFFFFF' ? 'White' : color) : 'Access restricted', children: state.color === color && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsxRuntime.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: jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) }) })) }, color))) })] }), jsxRuntime.jsxs("div", { className: "mb-4", children: [jsxRuntime.jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Background" }), disableBackgroundChange && (jsxRuntime.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: jsxRuntime.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 && (jsxRuntime.jsx("div", { className: "grid grid-cols-6 gap-1", children: backgroundColors.map((color) => (jsxRuntime.jsx("button", { className: `w-6 h-6 rounded border-2 transition-all relative ${!hasToolAccess
|
|
37277
37323
|
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37278
37324
|
: state.backgroundColor === color
|
|
37279
37325
|
? 'border-blue-400 dark:border-blue-500 scale-110'
|
|
@@ -42455,10 +42501,57 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42455
42501
|
}, [state.activeDrawings, state.lastClearTimestamp, dispatch]);
|
|
42456
42502
|
// Export functions to pass to toolbar
|
|
42457
42503
|
const handleExportImage = React.useCallback((format) => {
|
|
42458
|
-
if (boardRef.current) {
|
|
42504
|
+
if (!boardRef.current) {
|
|
42505
|
+
return;
|
|
42506
|
+
}
|
|
42507
|
+
// If video is active, capture both video and canvas
|
|
42508
|
+
if (videoStream && videoRef.current) {
|
|
42509
|
+
const video = videoRef.current;
|
|
42510
|
+
const canvas = document.createElement('canvas');
|
|
42511
|
+
const ctx = canvas.getContext('2d');
|
|
42512
|
+
if (!ctx)
|
|
42513
|
+
return;
|
|
42514
|
+
// Set canvas size to match the whiteboard
|
|
42515
|
+
canvas.width = video.videoWidth || video.offsetWidth;
|
|
42516
|
+
canvas.height = video.videoHeight || video.offsetHeight;
|
|
42517
|
+
try {
|
|
42518
|
+
// Draw the video frame
|
|
42519
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
42520
|
+
// Get the Konva stage data URL
|
|
42521
|
+
const stageDataURL = boardRef.current.getStageDataURL(format);
|
|
42522
|
+
// Create an image from the stage
|
|
42523
|
+
const stageImage = new Image();
|
|
42524
|
+
stageImage.onload = () => {
|
|
42525
|
+
// Draw the annotations on top of the video
|
|
42526
|
+
ctx.drawImage(stageImage, 0, 0, canvas.width, canvas.height);
|
|
42527
|
+
// Convert to blob and download
|
|
42528
|
+
canvas.toBlob((blob) => {
|
|
42529
|
+
if (blob) {
|
|
42530
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace('T', '_').replace(/:/g, '-');
|
|
42531
|
+
const filename = `whiteboard_video_annotation_${timestamp}.${format}`;
|
|
42532
|
+
const link = document.createElement('a');
|
|
42533
|
+
link.download = filename;
|
|
42534
|
+
link.href = URL.createObjectURL(blob);
|
|
42535
|
+
document.body.appendChild(link);
|
|
42536
|
+
link.click();
|
|
42537
|
+
document.body.removeChild(link);
|
|
42538
|
+
URL.revokeObjectURL(link.href);
|
|
42539
|
+
}
|
|
42540
|
+
}, format === 'png' ? 'image/png' : 'image/jpeg', 1.0);
|
|
42541
|
+
};
|
|
42542
|
+
stageImage.src = stageDataURL;
|
|
42543
|
+
}
|
|
42544
|
+
catch (error) {
|
|
42545
|
+
console.error('Failed to export video with annotations:', error);
|
|
42546
|
+
// Fallback to regular export
|
|
42547
|
+
boardRef.current.exportAsImage(format);
|
|
42548
|
+
}
|
|
42549
|
+
}
|
|
42550
|
+
else {
|
|
42551
|
+
// No video - regular export
|
|
42459
42552
|
boardRef.current.exportAsImage(format);
|
|
42460
42553
|
}
|
|
42461
|
-
}, []);
|
|
42554
|
+
}, [videoStream]);
|
|
42462
42555
|
const handleExportPDF = React.useCallback(() => {
|
|
42463
42556
|
if (boardRef.current) {
|
|
42464
42557
|
boardRef.current.exportAsPDF();
|