@ngenux/ngage-whiteboarding 1.0.8 → 1.0.10

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.
Files changed (51) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.esm.js +269 -124
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +275 -122
  6. package/dist/index.js.map +1 -1
  7. package/dist/src/components/Whiteboard/index.d.ts +5 -0
  8. package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
  9. package/dist/src/context/WhiteboardContext.d.ts +7 -0
  10. package/dist/src/context/WhiteboardContext.d.ts.map +1 -1
  11. package/dist/src/utils/socket-utility.d.ts +6 -1
  12. package/dist/src/utils/socket-utility.d.ts.map +1 -1
  13. package/dist/utils/index.d.ts +13 -0
  14. package/dist/utils/index.d.ts.map +1 -0
  15. package/dist/utils/socket-utility.esm.js +168 -0
  16. package/dist/utils/socket-utility.esm.js.map +1 -0
  17. package/dist/utils/socket-utility.js +177 -0
  18. package/dist/utils/socket-utility.js.map +1 -0
  19. package/dist/utils/src/components/Shapes/Arrow.d.ts +11 -0
  20. package/dist/utils/src/components/Shapes/Arrow.d.ts.map +1 -0
  21. package/dist/utils/src/components/Shapes/Ellipse.d.ts +11 -0
  22. package/dist/utils/src/components/Shapes/Ellipse.d.ts.map +1 -0
  23. package/dist/utils/src/components/Shapes/ErasedShape.d.ts +11 -0
  24. package/dist/utils/src/components/Shapes/ErasedShape.d.ts.map +1 -0
  25. package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts +11 -0
  26. package/dist/utils/src/components/Shapes/FreehandDrawing.d.ts.map +1 -0
  27. package/dist/utils/src/components/Shapes/Line.d.ts +11 -0
  28. package/dist/utils/src/components/Shapes/Line.d.ts.map +1 -0
  29. package/dist/utils/src/components/Shapes/Rectangle.d.ts +11 -0
  30. package/dist/utils/src/components/Shapes/Rectangle.d.ts.map +1 -0
  31. package/dist/utils/src/components/Whiteboard/Board.d.ts +15 -0
  32. package/dist/utils/src/components/Whiteboard/Board.d.ts.map +1 -0
  33. package/dist/utils/src/components/Whiteboard/Toolbar.d.ts +21 -0
  34. package/dist/utils/src/components/Whiteboard/Toolbar.d.ts.map +1 -0
  35. package/dist/utils/src/components/Whiteboard/index.d.ts +11 -0
  36. package/dist/utils/src/components/Whiteboard/index.d.ts.map +1 -0
  37. package/dist/utils/src/context/WhiteboardContext.d.ts +128 -0
  38. package/dist/utils/src/context/WhiteboardContext.d.ts.map +1 -0
  39. package/dist/utils/src/hooks/useCapture.d.ts +4 -0
  40. package/dist/utils/src/hooks/useCapture.d.ts.map +1 -0
  41. package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts +27 -0
  42. package/dist/utils/src/hooks/useCollaborativeWhiteboard.d.ts.map +1 -0
  43. package/dist/utils/src/lib/utils.d.ts +3 -0
  44. package/dist/utils/src/lib/utils.d.ts.map +1 -0
  45. package/dist/utils/src/types/index.d.ts +123 -0
  46. package/dist/utils/src/types/index.d.ts.map +1 -0
  47. package/dist/utils/src/utils/compression.d.ts +14 -0
  48. package/dist/utils/src/utils/compression.d.ts.map +1 -0
  49. package/dist/utils/src/utils/socket-utility.d.ts +11 -0
  50. package/dist/utils/src/utils/socket-utility.d.ts.map +1 -0
  51. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { WhiteboardProvider } from './src/context/WhiteboardContext';
4
4
  import { useCapture } from './src/hooks/useCapture';
5
5
  import { cn } from './src/lib/utils';
6
6
  export { Whiteboard, WhiteboardProvider, useCapture as useWhiteboardStream, cn };
7
+ export { isSocketConnected, getSocket, onSocketStatusChange, onSend, onReceive, leaveRoom, disconnectSocket, waitForSocket } from './src/utils/socket-utility';
7
8
  export type { WhiteboardProps } from './src/components/Whiteboard/index';
8
9
  export type { ShapeProps, ToolType, StrokeStyle, WhiteboardState, DrawingAction, CompressedData } from './src/types';
9
10
  export interface WhiteboardProviderProps {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,IAAI,mBAAmB,EAAE,EAAE,EAAE,CAAC;AAGjF,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,YAAY,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,IAAI,mBAAmB,EAAE,EAAE,EAAE,CAAC;AAGjF,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,oBAAoB,EACpB,MAAM,EACN,SAAS,EACT,SAAS,EACT,gBAAgB,EAChB,aAAa,EACd,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,YAAY,EACV,UAAU,EACV,QAAQ,EACR,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACf,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB"}
package/dist/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { createContext, useReducer, useContext, useState, useRef, useMemo, useEffect, forwardRef, useCallback, useImperativeHandle, createElement } from 'react';
3
+ import React__default, { createContext, useRef, useReducer, useEffect, useContext, useState, useMemo, forwardRef, useCallback, useImperativeHandle, createElement } from 'react';
4
4
 
5
5
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
6
6
 
@@ -34911,6 +34911,21 @@ const whiteboardReducer = (state, action) => {
34911
34911
  userUndoStacks: {}, // Clear all user undo stacks as well
34912
34912
  };
34913
34913
  }
34914
+ case 'RESET_EMPTY_ROOM': {
34915
+ // Same visual reset as a fresh board, but lastClearTimestamp stays 0 so shapes from
34916
+ // sync_state (drawn before this client joined) are not rejected as pre-clear stale.
34917
+ return {
34918
+ ...state,
34919
+ shapes: [],
34920
+ history: [[]],
34921
+ historyIndex: 0,
34922
+ selectedShapeId: undefined,
34923
+ currentDrawingShape: undefined,
34924
+ activeDrawings: {},
34925
+ userUndoStacks: {},
34926
+ lastClearTimestamp: 0,
34927
+ };
34928
+ }
34914
34929
  case 'UNDO': {
34915
34930
  if (state.historyIndex > 0) {
34916
34931
  return {
@@ -35116,8 +35131,120 @@ const whiteboardReducer = (state, action) => {
35116
35131
  }
35117
35132
  };
35118
35133
  const WhiteboardContext = createContext(undefined);
35119
- const WhiteboardProvider = ({ children, webSocketUrl }) => {
35120
- const [state, dispatch] = useReducer(whiteboardReducer, initialState);
35134
+ // Helper function to get storage key for a room
35135
+ const getStorageKey = (roomId) => {
35136
+ return roomId ? `whiteboard-state-${roomId}` : 'whiteboard-state-default';
35137
+ };
35138
+ // Helper function to load persisted state from sessionStorage
35139
+ const loadPersistedState = (roomId) => {
35140
+ try {
35141
+ const key = getStorageKey(roomId);
35142
+ const persisted = sessionStorage.getItem(key);
35143
+ if (persisted) {
35144
+ const parsed = JSON.parse(persisted);
35145
+ return parsed;
35146
+ }
35147
+ }
35148
+ catch (error) {
35149
+ console.error('[PERSISTENCE] Failed to load persisted state:', error);
35150
+ }
35151
+ return null;
35152
+ };
35153
+ // Helper function to save state to sessionStorage
35154
+ const savePersistedState = (state, roomId) => {
35155
+ try {
35156
+ const key = getStorageKey(roomId);
35157
+ // Only persist essential state (shapes, history, background, canvas size)
35158
+ const stateToPersist = {
35159
+ shapes: state.shapes,
35160
+ history: state.history,
35161
+ historyIndex: state.historyIndex,
35162
+ backgroundColor: state.backgroundColor,
35163
+ canvasSize: state.canvasSize,
35164
+ lastClearTimestamp: state.lastClearTimestamp,
35165
+ userUndoStacks: state.userUndoStacks,
35166
+ };
35167
+ sessionStorage.setItem(key, JSON.stringify(stateToPersist));
35168
+ }
35169
+ catch (error) {
35170
+ console.error('[PERSISTENCE] Failed to save persisted state:', error);
35171
+ }
35172
+ };
35173
+ // Helper function to clear persisted state
35174
+ const clearPersistedState = (roomId) => {
35175
+ try {
35176
+ const key = getStorageKey(roomId);
35177
+ sessionStorage.removeItem(key);
35178
+ }
35179
+ catch (error) {
35180
+ console.error('[PERSISTENCE] Failed to clear persisted state:', error);
35181
+ }
35182
+ };
35183
+ const WhiteboardProvider = ({ children, webSocketUrl, roomId: initialRoomId }) => {
35184
+ const [currentRoomId, setCurrentRoomId] = React__default.useState(initialRoomId);
35185
+ // Track if a clear action was performed
35186
+ const clearActionRef = useRef(false);
35187
+ // Load persisted state if available, otherwise use initialState
35188
+ const getInitialState = () => {
35189
+ const persisted = loadPersistedState(currentRoomId);
35190
+ if (persisted) {
35191
+ // Merge persisted state with current initialState to ensure all properties exist
35192
+ return {
35193
+ ...initialState,
35194
+ ...persisted,
35195
+ // Reset transient state
35196
+ isDrawing: false,
35197
+ currentDrawingShape: undefined,
35198
+ selectedShapeId: undefined,
35199
+ activeDrawings: {},
35200
+ captureEnabled: true,
35201
+ };
35202
+ }
35203
+ return initialState;
35204
+ };
35205
+ const [state, dispatch] = useReducer((s, action) => {
35206
+ // Track clear action
35207
+ if (action.type === 'CLEAR_CANVAS') {
35208
+ clearActionRef.current = true;
35209
+ }
35210
+ return whiteboardReducer(s, action);
35211
+ }, getInitialState());
35212
+ const prevRoomIdRef = useRef(currentRoomId);
35213
+ // Save state to sessionStorage whenever it changes (debounced)
35214
+ useEffect(() => {
35215
+ const timeoutId = setTimeout(() => {
35216
+ // Only save empty shapes if a clear action was performed
35217
+ if (state.shapes.length > 0 || clearActionRef.current) {
35218
+ savePersistedState(state, currentRoomId);
35219
+ clearActionRef.current = false;
35220
+ }
35221
+ }, 500); // Debounce to avoid excessive writes
35222
+ return () => clearTimeout(timeoutId);
35223
+ }, [state.shapes, state.history, state.backgroundColor, state.canvasSize, state.lastClearTimestamp, currentRoomId]);
35224
+ // Handle room changes - load new room state or clear if switching rooms
35225
+ useEffect(() => {
35226
+ if (prevRoomIdRef.current !== currentRoomId) {
35227
+ // Save current room state before switching
35228
+ if (prevRoomIdRef.current) {
35229
+ savePersistedState(state, prevRoomIdRef.current);
35230
+ }
35231
+ // Load new room state
35232
+ const newRoomState = loadPersistedState(currentRoomId);
35233
+ if (newRoomState) {
35234
+ dispatch({ type: 'SET_SHAPES', payload: newRoomState.shapes || [] });
35235
+ dispatch({ type: 'SET_BACKGROUND_COLOR', payload: newRoomState.backgroundColor || '#FFFFFF' });
35236
+ if (newRoomState.lastClearTimestamp) {
35237
+ dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: newRoomState.lastClearTimestamp });
35238
+ }
35239
+ }
35240
+ else {
35241
+ // No persisted state for new room — empty locally but do not use CLEAR_CANVAS
35242
+ // (that sets lastClearTimestamp = now and breaks applying peer sync_state for pre-join strokes).
35243
+ dispatch({ type: 'RESET_EMPTY_ROOM' });
35244
+ }
35245
+ prevRoomIdRef.current = currentRoomId;
35246
+ }
35247
+ }, [currentRoomId, state]);
35121
35248
  const normalizePoint = (point) => {
35122
35249
  return {
35123
35250
  x: point.x / state.canvasSize.width,
@@ -35198,7 +35325,7 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35198
35325
  };
35199
35326
  };
35200
35327
  // Store reference to current queue action for state requests
35201
- let currentQueueAction = null;
35328
+ const currentQueueActionRef = useRef(null);
35202
35329
  // Type guard and shape normalizer to ensure we have a complete ShapeProps object
35203
35330
  const isCompleteShape = (obj) => {
35204
35331
  if (!obj) {
@@ -35254,13 +35381,17 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35254
35381
  return true;
35255
35382
  };
35256
35383
  const requestStateFromPeers = () => {
35257
- if (currentQueueAction) {
35258
- currentQueueAction({
35259
- type: 'request_state',
35260
- payload: '',
35261
- requesterId: state.userId,
35262
- timestamp: Date.now(),
35263
- });
35384
+ if (currentQueueActionRef.current) {
35385
+ setTimeout(() => {
35386
+ if (currentQueueActionRef.current) {
35387
+ currentQueueActionRef.current({
35388
+ type: 'request_state',
35389
+ payload: '',
35390
+ requesterId: state.userId,
35391
+ timestamp: Date.now(),
35392
+ });
35393
+ }
35394
+ }, 2000);
35264
35395
  }
35265
35396
  else {
35266
35397
  console.warn('[STATE_SYNC] No queue action available for state request');
@@ -35292,9 +35423,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35292
35423
  switch (action.type) {
35293
35424
  case 'request_state':
35294
35425
  // Another user is requesting current state, send our state to them
35295
- if (typeof action.payload === 'string' && action.requesterId && currentQueueAction) {
35426
+ if (typeof action.payload === 'string' && action.requesterId && currentQueueActionRef.current) {
35296
35427
  const currentState = getCurrentSyncState();
35297
- currentQueueAction({
35428
+ currentQueueActionRef.current({
35298
35429
  type: 'sync_state',
35299
35430
  payload: currentState,
35300
35431
  requesterId: action.requesterId, // Send specifically to the requester
@@ -35368,16 +35499,19 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35368
35499
  console.warn(`[APPLY_ACTION] Failed to delete shape - payload not string:`, action.payload);
35369
35500
  }
35370
35501
  break;
35371
- case 'clear':
35372
- // Clear all canvas content including active drawings
35502
+ case 'clear': {
35503
+ // All users (not just host) must clear their local state when a collaborative clear is received
35373
35504
  dispatch({ type: 'CLEAR_CANVAS' });
35374
35505
  dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
35506
+ // Clear persisted state in sessionStorage
35507
+ clearPersistedState(currentRoomId);
35375
35508
  // Update last clear time to prevent conflicting actions
35376
35509
  if (typeof action.payload === 'object' && action.payload !== null && 'timestamp' in action.payload) {
35377
35510
  const clearTimestamp = action.payload.timestamp;
35378
35511
  dispatch({ type: 'SET_CLEAR_TIMESTAMP', payload: clearTimestamp });
35379
35512
  }
35380
35513
  break;
35514
+ }
35381
35515
  case 'undo':
35382
35516
  case 'redo':
35383
35517
  if (Array.isArray(action.payload)) {
@@ -35553,8 +35687,9 @@ const WhiteboardProvider = ({ children, webSocketUrl }) => {
35553
35687
  getCurrentSyncState,
35554
35688
  requestStateFromPeers,
35555
35689
  setQueueAction: (queueAction) => {
35556
- currentQueueAction = queueAction;
35690
+ currentQueueActionRef.current = queueAction;
35557
35691
  },
35692
+ setCurrentRoomId,
35558
35693
  normalizePoint,
35559
35694
  denormalizePoint,
35560
35695
  webSocketUrl,
@@ -37069,32 +37204,6 @@ const Eraser = createLucideIcon("Eraser", [
37069
37204
  */
37070
37205
 
37071
37206
 
37072
- const LockOpen = createLucideIcon("LockOpen", [
37073
- ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
37074
- ["path", { d: "M7 11V7a5 5 0 0 1 9.9-1", key: "1mm8w8" }]
37075
- ]);
37076
-
37077
- /**
37078
- * @license lucide-react v0.460.0 - ISC
37079
- *
37080
- * This source code is licensed under the ISC license.
37081
- * See the LICENSE file in the root directory of this source tree.
37082
- */
37083
-
37084
-
37085
- const Lock = createLucideIcon("Lock", [
37086
- ["rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2", key: "1w4ew1" }],
37087
- ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4", key: "fwvmzm" }]
37088
- ]);
37089
-
37090
- /**
37091
- * @license lucide-react v0.460.0 - ISC
37092
- *
37093
- * This source code is licensed under the ISC license.
37094
- * See the LICENSE file in the root directory of this source tree.
37095
- */
37096
-
37097
-
37098
37207
  const Minus = createLucideIcon("Minus", [["path", { d: "M5 12h14", key: "1ays0h" }]]);
37099
37208
 
37100
37209
  /**
@@ -37175,7 +37284,7 @@ const Undo2 = createLucideIcon("Undo2", [
37175
37284
  ]);
37176
37285
 
37177
37286
  // Top Toolbar Component
37178
- const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = false, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
37287
+ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockToggle, isAdmin = false, hasToolAccess = false, isGloballyUnlocked = true, shouldBeOpenByDefault = true, hasVideoBackground = false }) => {
37179
37288
  const { state, dispatch } = useWhiteboard();
37180
37289
  const [isVisible, setIsVisible] = useState(shouldBeOpenByDefault);
37181
37290
  const [isInitialized, setIsInitialized] = useState(false);
@@ -37251,9 +37360,7 @@ const TopToolbar = ({ queueAction, handleExportImage, handleClear, handleLockTog
37251
37360
  if (!isInitialized) {
37252
37361
  return null;
37253
37362
  }
37254
- 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
37255
- ? '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'
37256
- : '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
37363
+ 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: [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
37257
37364
  ? 'opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600'
37258
37365
  : state.tool === tool.type
37259
37366
  ? 'bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300'
@@ -42311,6 +42418,7 @@ Object.assign(lookup, {
42311
42418
  let socket = null;
42312
42419
  const joinedRooms = new Set();
42313
42420
  const setupCallbacks = new Map();
42421
+ const statusChangeCallbacks = new Set();
42314
42422
  let currentWebSocketUrl = undefined;
42315
42423
  // Initialize socket connection
42316
42424
  const initializeSocket = (webSocketUrl) => {
@@ -42326,7 +42434,6 @@ const initializeSocket = (webSocketUrl) => {
42326
42434
  setupCallbacks.clear();
42327
42435
  }
42328
42436
  currentWebSocketUrl = webSocketUrl;
42329
- console.log('[SOCKET] Using socket server URL:', socketServerUrl);
42330
42437
  if (!socketServerUrl) {
42331
42438
  console.error('[SOCKET] No socket server URL provided');
42332
42439
  }
@@ -42337,29 +42444,21 @@ const initializeSocket = (webSocketUrl) => {
42337
42444
  reconnectionDelay: 1000,
42338
42445
  });
42339
42446
  socket.on('connect', () => {
42340
- console.log('[SOCKET] Connected to server');
42341
- // Re-join all rooms after reconnection
42447
+ statusChangeCallbacks.forEach(callback => callback(true));
42342
42448
  joinedRooms.forEach(roomId => {
42343
42449
  socket.emit('join-room', roomId);
42344
- console.log('[SOCKET] Re-joined room:', roomId);
42345
42450
  });
42346
42451
  });
42347
42452
  socket.on('disconnect', () => {
42348
- console.log('[SOCKET] Disconnected from server');
42453
+ statusChangeCallbacks.forEach(callback => callback(false));
42349
42454
  });
42350
- socket.on('connect_error', (error) => {
42351
- console.error('[SOCKET] Connection error:', error);
42455
+ socket.on('connect_error', () => {
42456
+ // Connection error handled by Socket.IO reconnection logic
42352
42457
  });
42353
42458
  // Set up the global receive-message listener once
42354
42459
  socket.on('receive-message', (message) => {
42355
42460
  const callback = setupCallbacks.get(message.roomId);
42356
42461
  if (callback) {
42357
- console.log('[SOCKET] Received message from room:', message.roomId, {
42358
- compression: message.data.compressionType,
42359
- originalSize: message.data.originalSize,
42360
- compressedSize: message.data.compressedSize,
42361
- from: message.from
42362
- });
42363
42462
  callback(message.data);
42364
42463
  }
42365
42464
  });
@@ -42393,18 +42492,23 @@ const onSend = (roomId, data, webSocketUrl) => {
42393
42492
  console.error('[SOCKET] Error sending message:', error);
42394
42493
  }
42395
42494
  };
42396
- const onReceive = (roomId, callback, webSocketUrl) => {
42495
+ const onReceive = (roomId, callback, webSocketUrl, onRoomJoined) => {
42397
42496
  const socketInstance = initializeSocket(webSocketUrl);
42398
42497
  // Store the callback for this room
42399
42498
  setupCallbacks.set(roomId, callback);
42400
- // Only join the room if we haven't already
42401
42499
  if (!joinedRooms.has(roomId)) {
42402
42500
  socketInstance.emit('join-room', roomId);
42403
42501
  joinedRooms.add(roomId);
42404
- console.log('[SOCKET] Joined room:', roomId);
42502
+ if (onRoomJoined) {
42503
+ setTimeout(() => {
42504
+ onRoomJoined();
42505
+ }, 100);
42506
+ }
42405
42507
  }
42406
42508
  else {
42407
- console.log('[SOCKET] Already in room:', roomId);
42509
+ if (onRoomJoined) {
42510
+ onRoomJoined();
42511
+ }
42408
42512
  }
42409
42513
  };
42410
42514
  const leaveRoom = (roomId) => {
@@ -42421,24 +42525,74 @@ const disconnectSocket = () => {
42421
42525
  socket = null;
42422
42526
  joinedRooms.clear();
42423
42527
  setupCallbacks.clear();
42424
- console.log('[SOCKET] Socket disconnected and cleaned up');
42425
42528
  }
42426
42529
  };
42530
+ // Get current socket connection status
42531
+ const isSocketConnected = () => {
42532
+ return socket?.connected ?? false;
42533
+ };
42534
+ // Get current socket instance
42535
+ const getSocket = () => {
42536
+ return socket;
42537
+ };
42538
+ // Subscribe to connection status changes
42539
+ const onSocketStatusChange = (callback) => {
42540
+ const socketInstance = socket || initializeSocket();
42541
+ statusChangeCallbacks.add(callback);
42542
+ callback(socketInstance.connected);
42543
+ return () => {
42544
+ statusChangeCallbacks.delete(callback);
42545
+ };
42546
+ };
42547
+ const waitForSocket = (webSocketUrl, timeoutMs = 5000) => {
42548
+ return new Promise((resolve) => {
42549
+ const socketInstance = initializeSocket(webSocketUrl);
42550
+ if (socketInstance.connected) {
42551
+ resolve();
42552
+ return;
42553
+ }
42554
+ const handleConnect = () => {
42555
+ socketInstance.off('connect', handleConnect);
42556
+ clearTimeout(timeoutHandle);
42557
+ resolve();
42558
+ };
42559
+ socketInstance.on('connect', handleConnect);
42560
+ const timeoutHandle = setTimeout(() => {
42561
+ socketInstance.off('connect', handleConnect);
42562
+ resolve();
42563
+ }, timeoutMs);
42564
+ });
42565
+ };
42427
42566
 
42428
- const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream }) => {
42429
- const { state, dispatch, setQueueAction, requestStateFromPeers, webSocketUrl } = useWhiteboard();
42567
+ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transparentBackground = false, videoStream, externalApiRef }) => {
42568
+ const { state, dispatch, setQueueAction, requestStateFromPeers, setCurrentRoomId, webSocketUrl } = useWhiteboard();
42430
42569
  const [lastCollaborativeAction, setLastCollaborativeAction] = useState(null);
42431
42570
  const [hasRequestedState, setHasRequestedState] = useState(false);
42432
42571
  const [lastStateRequestTime, setLastStateRequestTime] = useState(0);
42433
- const [isGloballyUnlocked, setIsGloballyUnlocked] = useState(false); // Global unlock status
42572
+ const [isSocketConnected, setIsSocketConnected] = useState(false);
42573
+ const [isRoomJoined, setIsRoomJoined] = useState(false);
42574
+ const [isGloballyUnlocked, setIsGloballyUnlocked] = useState(true); // Global unlock status
42434
42575
  const [syncedAllowedUsers, setSyncedAllowedUsers] = useState(allowedUsers); // Synced allowed users from collaboration
42435
42576
  const isCollaborativeUpdateRef = useRef(false); // Track if update came from collaboration
42436
42577
  const lastClearTimeRef = useRef(0); // Track when last clear happened
42437
42578
  const boardRef = useRef(null);
42579
+ useEffect(() => {
42580
+ const unsubscribe = onSocketStatusChange((connected) => {
42581
+ setIsSocketConnected(connected);
42582
+ });
42583
+ return () => {
42584
+ console.log('[WHITEBOARD] Unsubscribing from socket status listener');
42585
+ unsubscribe();
42586
+ };
42587
+ }, []); // Empty dependencies - single listener for component lifecycle
42438
42588
  // Set userId in context when component mounts or userId changes
42439
42589
  useEffect(() => {
42440
42590
  dispatch({ type: 'SET_USER_ID', payload: userId });
42441
42591
  }, [userId, dispatch]);
42592
+ // Set current roomId in context for persistence
42593
+ useEffect(() => {
42594
+ setCurrentRoomId(roomId);
42595
+ }, [roomId, setCurrentRoomId]);
42442
42596
  // Set background color based on transparentBackground prop or videoStream presence
42443
42597
  useEffect(() => {
42444
42598
  if (transparentBackground || videoStream) {
@@ -42515,7 +42669,9 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42515
42669
  setHasRequestedState(false);
42516
42670
  }
42517
42671
  callback(data);
42518
- }, webSocketUrl);
42672
+ }, webSocketUrl, () => {
42673
+ setIsRoomJoined(true);
42674
+ });
42519
42675
  }, [roomId, webSocketUrl]);
42520
42676
  // Initialize the collaborative whiteboard hook
42521
42677
  const collaborativeConfig = useMemo(() => ({
@@ -42552,48 +42708,43 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42552
42708
  });
42553
42709
  }
42554
42710
  }, [allowedUsers, queueAction, userId, syncedAllowedUsers]);
42555
- // Request state from peers when joining/rejoining the room
42711
+ // Request state from peers when room is successfully joined AND socket is connected
42712
+ // Need both conditions to ensure message can actually be sent
42556
42713
  useEffect(() => {
42557
- if (queueAction && typeof queueAction === 'function' && !hasRequestedState) {
42558
- const requestTimer = setTimeout(() => {
42559
- const now = Date.now();
42560
- if (now - lastClearTimeRef.current < 10000) {
42561
- return;
42562
- }
42563
- if (now - lastStateRequestTime > 5000) {
42564
- requestStateFromPeers();
42565
- setHasRequestedState(true);
42566
- setLastStateRequestTime(now);
42567
- }
42568
- }, 500);
42569
- return () => clearTimeout(requestTimer);
42714
+ if (!isRoomJoined || !isSocketConnected || hasRequestedState || state.shapes.length > 0 || !queueAction) {
42715
+ return;
42716
+ }
42717
+ const now = Date.now();
42718
+ if (now - lastClearTimeRef.current < 10000) {
42719
+ return;
42570
42720
  }
42571
- }, [roomId, queueAction, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
42721
+ requestStateFromPeers();
42722
+ setHasRequestedState(true);
42723
+ setLastStateRequestTime(Date.now());
42724
+ }, [isRoomJoined, isSocketConnected, state.shapes.length, hasRequestedState, queueAction, requestStateFromPeers, userId, roomId]);
42572
42725
  // Reset request flag when room changes
42573
42726
  useEffect(() => {
42574
42727
  setHasRequestedState(false);
42575
42728
  setLastStateRequestTime(0);
42729
+ setIsRoomJoined(false);
42576
42730
  }, [roomId]);
42577
- // Monitor for empty canvas and request state only if we haven't tried recently
42731
+ // Retry state request if canvas is still empty (e.g. first request raced before host had queueAction).
42732
+ // Intentionally does NOT gate on hasRequestedState — the initial effect sets that immediately while send is delayed 2s.
42578
42733
  useEffect(() => {
42579
- if (queueAction &&
42580
- typeof queueAction === 'function' &&
42581
- state.shapes.length === 0 &&
42582
- !hasRequestedState) {
42583
- const now = Date.now();
42584
- if (now - lastClearTimeRef.current < 10000) {
42585
- return;
42586
- }
42587
- if (now - lastStateRequestTime > 10000) {
42588
- const reconnectTimer = setTimeout(() => {
42589
- requestStateFromPeers();
42590
- setHasRequestedState(true);
42591
- setLastStateRequestTime(now);
42592
- }, 2000);
42593
- return () => clearTimeout(reconnectTimer);
42594
- }
42734
+ if (!isRoomJoined || !isSocketConnected || state.shapes.length > 0 || !queueAction) {
42735
+ return;
42736
+ }
42737
+ const now = Date.now();
42738
+ // Don't retry if we recently cleared
42739
+ if (now - lastClearTimeRef.current < 10000) {
42740
+ return;
42595
42741
  }
42596
- }, [queueAction, state.shapes.length, requestStateFromPeers, hasRequestedState, lastStateRequestTime]);
42742
+ const retryTimer = setTimeout(() => {
42743
+ requestStateFromPeers();
42744
+ setLastStateRequestTime(Date.now());
42745
+ }, 4500);
42746
+ return () => clearTimeout(retryTimer);
42747
+ }, [isRoomJoined, isSocketConnected, state.shapes.length, queueAction, requestStateFromPeers, roomId]);
42597
42748
  // Cleanup stale active drawings periodically
42598
42749
  useEffect(() => {
42599
42750
  const cleanupInterval = setInterval(() => {
@@ -42692,6 +42843,22 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42692
42843
  });
42693
42844
  }
42694
42845
  }, [isAdmin, dispatch, queueAction, userId]);
42846
+ // Expose a minimal imperative API for host apps that need to perform the same
42847
+ // collaborative clear as the toolbar button (e.g., on "stop screenshare with annotations").
42848
+ useEffect(() => {
42849
+ if (!externalApiRef) {
42850
+ return;
42851
+ }
42852
+ externalApiRef.current = {
42853
+ clear: handleClear,
42854
+ getRoomId: () => roomId,
42855
+ };
42856
+ return () => {
42857
+ if (externalApiRef.current?.getRoomId?.() === roomId) {
42858
+ externalApiRef.current = null;
42859
+ }
42860
+ };
42861
+ }, [externalApiRef, handleClear, roomId]);
42695
42862
  const handleLockToggle = useCallback(() => {
42696
42863
  if (!isAdmin) {
42697
42864
  return;
@@ -42774,28 +42941,6 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
42774
42941
  leaveRoom(roomId);
42775
42942
  };
42776
42943
  }, [roomId]);
42777
- // Clear all canvases on component unmount if admin leaves
42778
- useEffect(() => {
42779
- return () => {
42780
- // If admin leaves, clear all users' canvases
42781
- if (isAdmin && queueAction) {
42782
- const clearTimestamp = Date.now();
42783
- // Clear local state immediately
42784
- dispatch({ type: 'CLEAR_CANVAS' });
42785
- dispatch({ type: 'CLEAR_ACTIVE_DRAWINGS' });
42786
- // Send clear action to all users
42787
- queueAction({
42788
- type: 'clear',
42789
- payload: {
42790
- timestamp: clearTimestamp,
42791
- adminId: userId,
42792
- },
42793
- userId: userId,
42794
- timestamp: clearTimestamp,
42795
- });
42796
- }
42797
- };
42798
- }, [isAdmin, queueAction, userId, dispatch]);
42799
42944
  // Global cleanup on app unmount
42800
42945
  useEffect(() => {
42801
42946
  const handleBeforeUnload = () => {
@@ -45838,5 +45983,5 @@ function cn(...inputs) {
45838
45983
  return twMerge(clsx(inputs));
45839
45984
  }
45840
45985
 
45841
- export { Whiteboard, WhiteboardProvider, cn, useCapture as useWhiteboardStream };
45986
+ export { Whiteboard, WhiteboardProvider, cn, disconnectSocket, getSocket, isSocketConnected, leaveRoom, onReceive, onSend, onSocketStatusChange, useCapture as useWhiteboardStream, waitForSocket };
45842
45987
  //# sourceMappingURL=index.esm.js.map