@tsdraw/react 0.8.4 → 0.9.0

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.cjs CHANGED
@@ -303,7 +303,11 @@ var TAP_MOVE_TOLERANCE = 14;
303
303
  var PINCH_MODE_ZOOM_DISTANCE = 24;
304
304
  var PINCH_MODE_PAN_DISTANCE = 16;
305
305
  var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
306
- function createTouchInteractionController(editor, canvas, handlers) {
306
+ function createTouchInteractionController(editor, canvas, handlers, touchOptions) {
307
+ const allowPinchZoom = touchOptions?.pinchToZoom !== false;
308
+ const allowFingerPan = touchOptions?.fingerPanInPenMode !== false;
309
+ const allowTapUndoRedo = touchOptions?.tapUndoRedo !== false;
310
+ const allowTrackpadGestures = touchOptions?.trackpadGestures !== false;
307
311
  const activeTouchPoints = /* @__PURE__ */ new Map();
308
312
  const touchTapState = {
309
313
  active: false,
@@ -345,7 +349,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
345
349
  if (activeTouchPoints.size > 0) return;
346
350
  if (!touchTapState.active) return;
347
351
  const elapsed = performance.now() - touchTapState.startTime;
348
- if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
352
+ if (allowTapUndoRedo && !touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
349
353
  const fingerCount = touchTapState.maxTouchCount;
350
354
  const now = performance.now();
351
355
  const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
@@ -393,9 +397,9 @@ function createTouchInteractionController(editor, canvas, handlers) {
393
397
  const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
394
398
  const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
395
399
  if (touchCameraState.mode === "not-sure") {
396
- if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
400
+ if (allowPinchZoom && touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
397
401
  else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
398
- } else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
402
+ } else if (allowPinchZoom && touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
399
403
  const canvasRect = canvas.getBoundingClientRect();
400
404
  const centerOnCanvasX = center.x - canvasRect.left;
401
405
  const centerOnCanvasY = center.y - canvasRect.top;
@@ -436,7 +440,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
436
440
  beginTouchCameraGesture();
437
441
  return true;
438
442
  }
439
- if (handlers.isPenModeActive() && activeTouchPoints.size === 1) {
443
+ if (allowFingerPan && handlers.isPenModeActive() && activeTouchPoints.size === 1) {
440
444
  handlers.cancelActivePointerInteraction();
441
445
  fingerPanPointerId = event.pointerId;
442
446
  fingerPanSession = core.beginCameraPan(editor.viewport, event.clientX, event.clientY);
@@ -471,11 +475,15 @@ function createTouchInteractionController(editor, canvas, handlers) {
471
475
  if (wasFingerPan) {
472
476
  endFingerPan();
473
477
  if (releasedPanSession) {
474
- fingerPanSlide = core.startCameraSlide(
475
- releasedPanSession,
476
- (dx, dy) => editor.panBy(dx, dy),
477
- () => handlers.refreshView()
478
- );
478
+ const slideConfig = handlers.getSlideOptions();
479
+ if (slideConfig.enabled) {
480
+ fingerPanSlide = core.startCameraSlide(
481
+ releasedPanSession,
482
+ (dx, dy) => editor.panBy(dx, dy),
483
+ () => handlers.refreshView(),
484
+ slideConfig.slideOptions
485
+ );
486
+ }
479
487
  }
480
488
  }
481
489
  maybeHandleTouchTapGesture();
@@ -486,6 +494,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
486
494
  const handleGestureEvent = (event, container) => {
487
495
  if (!container.contains(event.target)) return;
488
496
  event.preventDefault();
497
+ if (!allowTrackpadGestures) return;
489
498
  const gestureEvent = event;
490
499
  if (gestureEvent.scale == null) return;
491
500
  if (event.type === "gesturestart") {
@@ -529,7 +538,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
529
538
  }
530
539
 
531
540
  // src/canvas/keyboardShortcuts.ts
532
- var TOOL_SHORTCUTS = {
541
+ var DEFAULT_TOOL_SHORTCUTS = {
533
542
  v: "select",
534
543
  h: "hand",
535
544
  e: "eraser",
@@ -541,6 +550,11 @@ var TOOL_SHORTCUTS = {
541
550
  o: "circle",
542
551
  c: "circle"
543
552
  };
553
+ function resolveToolShortcuts(shortcutOptions) {
554
+ if (!shortcutOptions?.toolShortcuts) return DEFAULT_TOOL_SHORTCUTS;
555
+ if (shortcutOptions.overrideDefaults) return { ...shortcutOptions.toolShortcuts };
556
+ return { ...DEFAULT_TOOL_SHORTCUTS, ...shortcutOptions.toolShortcuts };
557
+ }
544
558
  function isEditableTarget(eventTarget) {
545
559
  const element = eventTarget;
546
560
  if (!element) return false;
@@ -548,7 +562,7 @@ function isEditableTarget(eventTarget) {
548
562
  const tagName = element.tagName;
549
563
  return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
550
564
  }
551
- function handleKeyboardShortcutKeyDown(event, handlers) {
565
+ function handleKeyboardShortcutKeyDown(event, handlers, toolShortcutMap = DEFAULT_TOOL_SHORTCUTS) {
552
566
  if (isEditableTarget(event.target)) return;
553
567
  const loweredKey = event.key.toLowerCase();
554
568
  const isMetaPressed = event.metaKey || event.ctrlKey;
@@ -561,7 +575,7 @@ function handleKeyboardShortcutKeyDown(event, handlers) {
561
575
  }
562
576
  }
563
577
  if (!isMetaPressed && !event.altKey) {
564
- const nextToolId = TOOL_SHORTCUTS[loweredKey];
578
+ const nextToolId = toolShortcutMap[loweredKey];
565
579
  if (nextToolId && handlers.isToolAvailable(nextToolId)) {
566
580
  handlers.setToolFromShortcut(nextToolId);
567
581
  event.preventDefault();
@@ -736,8 +750,18 @@ function getHandlePagePoint(bounds, handle) {
736
750
  }
737
751
  }
738
752
  var ZOOM_WHEEL_CAP = 10;
753
+ var VIEW_ONLY_TOOLS = /* @__PURE__ */ new Set(["select", "hand"]);
739
754
  function useTsdrawCanvasController(options = {}) {
740
755
  const onMountRef = react.useRef(options.onMount);
756
+ const onChangeRef = react.useRef(options.onChange);
757
+ const onCameraChangeRef = react.useRef(options.onCameraChange);
758
+ const onToolChangeRef = react.useRef(options.onToolChange);
759
+ const cameraOptionsRef = react.useRef(options.cameraOptions);
760
+ const touchOptionsRef = react.useRef(options.touchOptions);
761
+ const keyboardShortcutsRef = react.useRef(options.keyboardShortcuts);
762
+ const penOptionsRef = react.useRef(options.penOptions);
763
+ const backgroundRef = react.useRef(options.background);
764
+ const readOnlyRef = react.useRef(options.readOnly ?? false);
741
765
  const containerRef = react.useRef(null);
742
766
  const canvasRef = react.useRef(null);
743
767
  const editorRef = react.useRef(null);
@@ -797,6 +821,33 @@ function useTsdrawCanvasController(options = {}) {
797
821
  react.useEffect(() => {
798
822
  onMountRef.current = options.onMount;
799
823
  }, [options.onMount]);
824
+ react.useEffect(() => {
825
+ onChangeRef.current = options.onChange;
826
+ }, [options.onChange]);
827
+ react.useEffect(() => {
828
+ onCameraChangeRef.current = options.onCameraChange;
829
+ }, [options.onCameraChange]);
830
+ react.useEffect(() => {
831
+ onToolChangeRef.current = options.onToolChange;
832
+ }, [options.onToolChange]);
833
+ react.useEffect(() => {
834
+ cameraOptionsRef.current = options.cameraOptions;
835
+ }, [options.cameraOptions]);
836
+ react.useEffect(() => {
837
+ touchOptionsRef.current = options.touchOptions;
838
+ }, [options.touchOptions]);
839
+ react.useEffect(() => {
840
+ keyboardShortcutsRef.current = options.keyboardShortcuts;
841
+ }, [options.keyboardShortcuts]);
842
+ react.useEffect(() => {
843
+ penOptionsRef.current = options.penOptions;
844
+ }, [options.penOptions]);
845
+ react.useEffect(() => {
846
+ backgroundRef.current = options.background;
847
+ }, [options.background]);
848
+ react.useEffect(() => {
849
+ readOnlyRef.current = options.readOnly ?? false;
850
+ }, [options.readOnly]);
800
851
  react.useEffect(() => {
801
852
  selectedShapeIdsRef.current = selectedShapeIds;
802
853
  }, [selectedShapeIds]);
@@ -813,8 +864,11 @@ function useTsdrawCanvasController(options = {}) {
813
864
  const ctx = canvas.getContext("2d");
814
865
  if (!ctx) return;
815
866
  const dpr = dprRef.current || 1;
867
+ const logicalWidth = canvas.width / dpr;
868
+ const logicalHeight = canvas.height / dpr;
816
869
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
817
- ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
870
+ ctx.clearRect(0, 0, logicalWidth, logicalHeight);
871
+ core.renderCanvasBackground(ctx, editor.viewport, logicalWidth, logicalHeight, backgroundRef.current, editor.renderer.theme);
818
872
  editor.render(ctx);
819
873
  }, []);
820
874
  const refreshSelectionBounds = react.useCallback((editor, ids = selectedShapeIdsRef.current) => {
@@ -920,14 +974,21 @@ function useTsdrawCanvasController(options = {}) {
920
974
  const canvas = canvasRef.current;
921
975
  if (!container || !canvas) return;
922
976
  const initialTool = options.initialTool ?? "pen";
977
+ const cameraOpts = cameraOptionsRef.current;
978
+ const touchOpts = touchOptionsRef.current;
979
+ const toolShortcutMap = resolveToolShortcuts(keyboardShortcutsRef.current);
923
980
  const editor = new core.Editor({
924
981
  toolDefinitions: options.toolDefinitions,
925
- initialToolId: initialTool
982
+ initialToolId: initialTool,
983
+ zoomRange: cameraOpts?.zoomRange
926
984
  });
927
985
  editor.renderer.setTheme(options.theme ?? "light");
928
986
  if (!editor.tools.hasTool(initialTool)) {
929
987
  editor.setCurrentTool("pen");
930
988
  }
989
+ if (options.snapshot) {
990
+ editor.loadPersistenceSnapshot(options.snapshot);
991
+ }
931
992
  let disposed = false;
932
993
  let ignorePersistenceChanges = false;
933
994
  let disposeMount;
@@ -1094,16 +1155,24 @@ function useTsdrawCanvasController(options = {}) {
1094
1155
  render();
1095
1156
  refreshSelectionBounds(editor);
1096
1157
  };
1158
+ const emitCameraChange = () => {
1159
+ onCameraChangeRef.current?.({ ...editor.viewport });
1160
+ };
1097
1161
  const touchInteractions = createTouchInteractionController(editor, canvas, {
1098
1162
  cancelActivePointerInteraction,
1099
1163
  refreshView: () => {
1100
1164
  render();
1101
1165
  refreshSelectionBounds(editor);
1166
+ emitCameraChange();
1102
1167
  },
1103
1168
  runUndo: () => applyDocumentChangeResult(editor.undo()),
1104
1169
  runRedo: () => applyDocumentChangeResult(editor.redo()),
1105
- isPenModeActive: () => penModeRef.current
1106
- });
1170
+ isPenModeActive: () => penModeRef.current,
1171
+ getSlideOptions: () => ({
1172
+ enabled: cameraOptionsRef.current?.slideEnabled !== false,
1173
+ slideOptions: { friction: cameraOptionsRef.current?.slideFriction }
1174
+ })
1175
+ }, touchOpts);
1107
1176
  const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
1108
1177
  const stopActiveSlide = () => {
1109
1178
  if (activeCameraSlideRef.current) {
@@ -1113,8 +1182,10 @@ function useTsdrawCanvasController(options = {}) {
1113
1182
  };
1114
1183
  const handlePointerDown = (e) => {
1115
1184
  if (!canvas.contains(e.target)) return;
1185
+ if (cameraOptionsRef.current?.locked && e.pointerType !== "pen") return;
1116
1186
  stopActiveSlide();
1117
- if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1187
+ const penAutoDetect = penOptionsRef.current?.autoDetect !== false;
1188
+ if (penAutoDetect && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1118
1189
  penDetectedRef.current = true;
1119
1190
  penModeRef.current = true;
1120
1191
  }
@@ -1135,6 +1206,9 @@ function useTsdrawCanvasController(options = {}) {
1135
1206
  if (activePointerIdsRef.current.size > 1) {
1136
1207
  return;
1137
1208
  }
1209
+ if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(currentToolRef.current)) {
1210
+ return;
1211
+ }
1138
1212
  isPointerActiveRef.current = true;
1139
1213
  editor.beginHistoryEntry();
1140
1214
  canvas.setPointerCapture(e.pointerId);
@@ -1142,7 +1216,8 @@ function useTsdrawCanvasController(options = {}) {
1142
1216
  updatePointerPreview(e.clientX, e.clientY);
1143
1217
  const first = sampleEvents(e)[0];
1144
1218
  const { x, y } = getPagePoint(first);
1145
- const pressure = first.pressure ?? 0.5;
1219
+ const pressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
1220
+ const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
1146
1221
  const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
1147
1222
  if (currentToolRef.current === "select") {
1148
1223
  const hit = core.getTopShapeAtPoint(editor, { x, y });
@@ -1189,7 +1264,8 @@ function useTsdrawCanvasController(options = {}) {
1189
1264
  refreshSelectionBounds(editor);
1190
1265
  };
1191
1266
  const handlePointerMove = (e) => {
1192
- if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1267
+ const penAutoDetectOnMove = penOptionsRef.current?.autoDetect !== false;
1268
+ if (penAutoDetectOnMove && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1193
1269
  penDetectedRef.current = true;
1194
1270
  penModeRef.current = true;
1195
1271
  }
@@ -1204,9 +1280,10 @@ function useTsdrawCanvasController(options = {}) {
1204
1280
  const dx = prevClient ? e.clientX - prevClient.x : 0;
1205
1281
  const dy = prevClient ? e.clientY - prevClient.y : 0;
1206
1282
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
1283
+ const movePressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
1207
1284
  for (const sample of sampleEvents(e)) {
1208
1285
  const { x, y } = getPagePoint(sample);
1209
- const pressure = sample.pressure ?? 0.5;
1286
+ const pressure = (sample.pressure ?? 0.5) * movePressureSensitivity;
1210
1287
  const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
1211
1288
  editor.input.pointerMove(x, y, pressure, isPen);
1212
1289
  }
@@ -1340,14 +1417,18 @@ function useTsdrawCanvasController(options = {}) {
1340
1417
  editor.tools.pointerUp();
1341
1418
  render();
1342
1419
  refreshSelectionBounds(editor);
1343
- if (handPanSession) {
1420
+ if (handPanSession && cameraOptionsRef.current?.slideEnabled !== false) {
1344
1421
  activeCameraSlideRef.current = core.startCameraSlide(
1345
1422
  handPanSession,
1346
- (slideDx, slideDy) => editor.panBy(slideDx, slideDy),
1423
+ (slideDx, slideDy) => {
1424
+ editor.panBy(slideDx, slideDy);
1425
+ emitCameraChange();
1426
+ },
1347
1427
  () => {
1348
1428
  render();
1349
1429
  refreshSelectionBounds(editor);
1350
- }
1430
+ },
1431
+ { friction: cameraOptionsRef.current?.slideFriction }
1351
1432
  );
1352
1433
  }
1353
1434
  if (pendingRemoteDocumentRef.current) {
@@ -1392,41 +1473,59 @@ function useTsdrawCanvasController(options = {}) {
1392
1473
  const handleWheel = (e) => {
1393
1474
  if (!container.contains(e.target)) return;
1394
1475
  e.preventDefault();
1476
+ const camOpts = cameraOptionsRef.current;
1477
+ if (camOpts?.locked) return;
1478
+ if (camOpts?.wheelBehavior === "none") return;
1395
1479
  if (touchInteractions.isTrackpadZoomActive()) return;
1396
1480
  const delta = normalizeWheelDelta(e);
1481
+ const panMultiplier = camOpts?.panSpeed ?? 1;
1482
+ const zoomMultiplier = camOpts?.zoomSpeed ?? 1;
1397
1483
  if (delta.z !== 0) {
1398
1484
  const rect = canvas.getBoundingClientRect();
1399
1485
  const pointX = e.clientX - rect.left;
1400
1486
  const pointY = e.clientY - rect.top;
1401
- editor.zoomAt(Math.exp(delta.z), pointX, pointY);
1487
+ editor.zoomAt(Math.exp(delta.z * zoomMultiplier), pointX, pointY);
1402
1488
  } else {
1403
- editor.panBy(delta.x, delta.y);
1489
+ editor.panBy(delta.x * panMultiplier, delta.y * panMultiplier);
1404
1490
  }
1405
1491
  render();
1406
1492
  refreshSelectionBounds(editor);
1493
+ emitCameraChange();
1407
1494
  };
1408
1495
  const handleGestureEvent = (e) => {
1409
1496
  touchInteractions.handleGestureEvent(e, container);
1410
1497
  };
1411
1498
  const handleKeyDown = (e) => {
1499
+ if (keyboardShortcutsRef.current?.enabled === false) return;
1500
+ const isReadOnly = readOnlyRef.current;
1412
1501
  handleKeyboardShortcutKeyDown(e, {
1413
- isToolAvailable: (tool) => editor.tools.hasTool(tool),
1502
+ isToolAvailable: (tool) => {
1503
+ if (isReadOnly && !VIEW_ONLY_TOOLS.has(tool)) return false;
1504
+ return editor.tools.hasTool(tool);
1505
+ },
1414
1506
  setToolFromShortcut: (tool) => {
1415
1507
  editor.setCurrentTool(tool);
1416
1508
  setCurrentToolState(tool);
1417
1509
  currentToolRef.current = tool;
1418
1510
  if (tool !== "select") resetSelectUi();
1419
1511
  render();
1512
+ onToolChangeRef.current?.(tool);
1513
+ },
1514
+ runHistoryShortcut: (shouldRedo) => {
1515
+ if (isReadOnly) return false;
1516
+ return applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo());
1517
+ },
1518
+ deleteSelection: () => {
1519
+ if (isReadOnly) return false;
1520
+ return currentToolRef.current === "select" ? deleteCurrentSelection() : false;
1420
1521
  },
1421
- runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
1422
- deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
1423
1522
  dispatchKeyDown: (event) => {
1424
1523
  editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1425
1524
  editor.tools.keyDown({ key: event.key });
1426
1525
  render();
1427
1526
  },
1428
1527
  dispatchKeyUp: () => void 0
1429
- });
1528
+ }, toolShortcutMap);
1430
1529
  };
1431
1530
  const handleKeyUp = (e) => {
1432
1531
  handleKeyboardShortcutKeyUp(e, {
@@ -1507,6 +1606,7 @@ function useTsdrawCanvasController(options = {}) {
1507
1606
  const cleanupEditorListener = editor.listen(() => {
1508
1607
  if (ignorePersistenceChanges) return;
1509
1608
  schedulePersist();
1609
+ onChangeRef.current?.(editor.getDocumentSnapshot());
1510
1610
  });
1511
1611
  const cleanupHistoryListener = editor.listenHistory(() => {
1512
1612
  syncHistoryState();
@@ -1573,6 +1673,9 @@ function useTsdrawCanvasController(options = {}) {
1573
1673
  render();
1574
1674
  }
1575
1675
  });
1676
+ if (options.autoFocus !== false) {
1677
+ container.focus({ preventScroll: true });
1678
+ }
1576
1679
  return () => {
1577
1680
  disposed = true;
1578
1681
  schedulePersistRef.current = null;
@@ -1616,15 +1719,21 @@ function useTsdrawCanvasController(options = {}) {
1616
1719
  editor.renderer.setTheme(options.theme ?? "light");
1617
1720
  render();
1618
1721
  }, [options.theme, render]);
1722
+ react.useEffect(() => {
1723
+ if (!editorRef.current) return;
1724
+ render();
1725
+ }, [options.background, render]);
1619
1726
  const setTool = react.useCallback(
1620
1727
  (tool) => {
1621
1728
  const editor = editorRef.current;
1622
1729
  if (!editor) return;
1623
1730
  if (!editor.tools.hasTool(tool)) return;
1731
+ if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(tool)) return;
1624
1732
  editor.setCurrentTool(tool);
1625
1733
  setCurrentToolState(tool);
1626
1734
  currentToolRef.current = tool;
1627
1735
  if (tool !== "select") resetSelectUi();
1736
+ onToolChangeRef.current?.(tool);
1628
1737
  },
1629
1738
  [resetSelectUi]
1630
1739
  );
@@ -1860,12 +1969,23 @@ function Tsdraw(props) {
1860
1969
  initialTool,
1861
1970
  theme: resolvedTheme,
1862
1971
  persistenceKey: props.persistenceKey,
1863
- onMount: props.onMount
1972
+ onMount: props.onMount,
1973
+ cameraOptions: props.cameraOptions,
1974
+ touchOptions: props.touchOptions,
1975
+ keyboardShortcuts: props.keyboardShortcuts,
1976
+ penOptions: props.penOptions,
1977
+ background: props.background,
1978
+ readOnly: props.readOnly,
1979
+ autoFocus: props.autoFocus,
1980
+ snapshot: props.snapshot,
1981
+ onChange: props.onChange,
1982
+ onCameraChange: props.onCameraChange,
1983
+ onToolChange: props.onToolChange
1864
1984
  });
1865
1985
  const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
1866
1986
  const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
1867
1987
  const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
1868
- const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
1988
+ const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
1869
1989
  const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
1870
1990
  const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
1871
1991
  ToolOverlay,
@@ -1954,12 +2074,14 @@ function Tsdraw(props) {
1954
2074
  "div",
1955
2075
  {
1956
2076
  ref: containerRef,
2077
+ tabIndex: 0,
1957
2078
  className: `tsdraw tsdraw-${resolvedTheme}mode ${props.className ?? ""}`,
1958
2079
  style: {
1959
2080
  width: props.width ?? "100%",
1960
2081
  height: props.height ?? "100%",
1961
2082
  position: "relative",
1962
2083
  overflow: "hidden",
2084
+ outline: "none",
1963
2085
  ...props.style
1964
2086
  },
1965
2087
  children: [