@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/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 border-2 transition-all ${!hasToolAccess
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-purple-400 dark:border-purple-500 scale-110'
37312
+ ? 'border-[3px] border-yellow-400 dark:border-yellow-300 scale-125 shadow-lg'
37271
37313
  : color === '#FFFFFF'
37272
- ? 'border-gray-400 dark:border-gray-500 hover:border-gray-500 dark:hover:border-gray-400'
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' ? 'inset 0 0 0 1px rgba(0,0,0,0.1)' : 'none'
37276
- }, onClick: () => handleColorChange(color), disabled: !hasToolAccess, title: hasToolAccess ? (color === '#FFFFFF' ? 'White' : color) : 'Access restricted' }, 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
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();