@tsdraw/react 0.8.4 → 0.8.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.d.cts CHANGED
@@ -1,6 +1,31 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
- import { ColorStyle, DashStyle, FillStyle, SizeStyle, ToolId, Editor, ToolDefinition } from '@tsdraw/core';
3
+ import { ZoomRange, ToolId, ColorStyle, DashStyle, FillStyle, SizeStyle, Editor, ToolDefinition, TsdrawEditorSnapshot, TsdrawDocumentSnapshot, Viewport } from '@tsdraw/core';
4
+
5
+ interface TsdrawCameraOptions {
6
+ panSpeed?: number;
7
+ zoomSpeed?: number;
8
+ zoomRange?: ZoomRange;
9
+ wheelBehavior?: 'pan' | 'zoom' | 'none';
10
+ slideEnabled?: boolean;
11
+ slideFriction?: number;
12
+ locked?: boolean;
13
+ }
14
+ interface TsdrawTouchOptions {
15
+ pinchToZoom?: boolean;
16
+ fingerPanInPenMode?: boolean;
17
+ tapUndoRedo?: boolean;
18
+ trackpadGestures?: boolean;
19
+ }
20
+ interface TsdrawKeyboardShortcutOptions {
21
+ enabled?: boolean;
22
+ toolShortcuts?: Record<string, ToolId>;
23
+ overrideDefaults?: boolean;
24
+ }
25
+ interface TsdrawPenOptions {
26
+ pressureSensitivity?: number;
27
+ autoDetect?: boolean;
28
+ }
4
29
 
5
30
  type TsdrawStylePanelPartItem = 'colors' | 'dashes' | 'fills' | 'sizes' | (string & {});
6
31
  interface TsdrawStylePanelRenderContext {
@@ -123,6 +148,16 @@ interface TsdrawProps {
123
148
  initialToolId?: ToolId;
124
149
  uiOptions?: TsdrawUiOptions;
125
150
  onMount?: (api: TsdrawMountApi) => void | (() => void);
151
+ cameraOptions?: TsdrawCameraOptions;
152
+ touchOptions?: TsdrawTouchOptions;
153
+ keyboardShortcuts?: TsdrawKeyboardShortcutOptions;
154
+ penOptions?: TsdrawPenOptions;
155
+ readOnly?: boolean;
156
+ autoFocus?: boolean;
157
+ snapshot?: TsdrawEditorSnapshot;
158
+ onChange?: (snapshot: TsdrawDocumentSnapshot) => void;
159
+ onCameraChange?: (viewport: Viewport) => void;
160
+ onToolChange?: (toolId: ToolId) => void;
126
161
  }
127
162
  type TsdrawCanvasProps = TsdrawProps;
128
163
  declare function Tsdraw(props: TsdrawProps): react_jsx_runtime.JSX.Element;
@@ -148,4 +183,4 @@ interface ToolbarPart {
148
183
  }
149
184
  declare function getDefaultToolbarIcon(toolId: ToolId, isActive: boolean): ReactNode;
150
185
 
151
- export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawMountApi, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
186
+ export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, type TsdrawCameraOptions, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawKeyboardShortcutOptions, type TsdrawMountApi, type TsdrawPenOptions, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawTouchOptions, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,31 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, CSSProperties } from 'react';
3
- import { ColorStyle, DashStyle, FillStyle, SizeStyle, ToolId, Editor, ToolDefinition } from '@tsdraw/core';
3
+ import { ZoomRange, ToolId, ColorStyle, DashStyle, FillStyle, SizeStyle, Editor, ToolDefinition, TsdrawEditorSnapshot, TsdrawDocumentSnapshot, Viewport } from '@tsdraw/core';
4
+
5
+ interface TsdrawCameraOptions {
6
+ panSpeed?: number;
7
+ zoomSpeed?: number;
8
+ zoomRange?: ZoomRange;
9
+ wheelBehavior?: 'pan' | 'zoom' | 'none';
10
+ slideEnabled?: boolean;
11
+ slideFriction?: number;
12
+ locked?: boolean;
13
+ }
14
+ interface TsdrawTouchOptions {
15
+ pinchToZoom?: boolean;
16
+ fingerPanInPenMode?: boolean;
17
+ tapUndoRedo?: boolean;
18
+ trackpadGestures?: boolean;
19
+ }
20
+ interface TsdrawKeyboardShortcutOptions {
21
+ enabled?: boolean;
22
+ toolShortcuts?: Record<string, ToolId>;
23
+ overrideDefaults?: boolean;
24
+ }
25
+ interface TsdrawPenOptions {
26
+ pressureSensitivity?: number;
27
+ autoDetect?: boolean;
28
+ }
4
29
 
5
30
  type TsdrawStylePanelPartItem = 'colors' | 'dashes' | 'fills' | 'sizes' | (string & {});
6
31
  interface TsdrawStylePanelRenderContext {
@@ -123,6 +148,16 @@ interface TsdrawProps {
123
148
  initialToolId?: ToolId;
124
149
  uiOptions?: TsdrawUiOptions;
125
150
  onMount?: (api: TsdrawMountApi) => void | (() => void);
151
+ cameraOptions?: TsdrawCameraOptions;
152
+ touchOptions?: TsdrawTouchOptions;
153
+ keyboardShortcuts?: TsdrawKeyboardShortcutOptions;
154
+ penOptions?: TsdrawPenOptions;
155
+ readOnly?: boolean;
156
+ autoFocus?: boolean;
157
+ snapshot?: TsdrawEditorSnapshot;
158
+ onChange?: (snapshot: TsdrawDocumentSnapshot) => void;
159
+ onCameraChange?: (viewport: Viewport) => void;
160
+ onToolChange?: (toolId: ToolId) => void;
126
161
  }
127
162
  type TsdrawCanvasProps = TsdrawProps;
128
163
  declare function Tsdraw(props: TsdrawProps): react_jsx_runtime.JSX.Element;
@@ -148,4 +183,4 @@ interface ToolbarPart {
148
183
  }
149
184
  declare function getDefaultToolbarIcon(toolId: ToolId, isActive: boolean): ReactNode;
150
185
 
151
- export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawMountApi, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
186
+ export { type ToolbarActionItem, type ToolbarPart, type ToolbarPartItem, type ToolbarRenderItem, type ToolbarToolItem, Tsdraw, type TsdrawCameraOptions, TsdrawCanvas, type TsdrawCanvasProps, type TsdrawCursorContext, type TsdrawCustomElement, type TsdrawCustomElementRenderArgs, type TsdrawCustomTool, type TsdrawKeyboardShortcutOptions, type TsdrawMountApi, type TsdrawPenOptions, type TsdrawProps, type TsdrawStylePanelCustomPart, type TsdrawStylePanelPartItem, type TsdrawStylePanelRenderContext, type TsdrawToolOverlayState, type TsdrawToolbarBuiltInAction, type TsdrawTouchOptions, type TsdrawUiOptions, type TsdrawUiPlacement, type UiAnchor, getDefaultToolbarIcon };
package/dist/index.js CHANGED
@@ -301,7 +301,11 @@ var TAP_MOVE_TOLERANCE = 14;
301
301
  var PINCH_MODE_ZOOM_DISTANCE = 24;
302
302
  var PINCH_MODE_PAN_DISTANCE = 16;
303
303
  var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
304
- function createTouchInteractionController(editor, canvas, handlers) {
304
+ function createTouchInteractionController(editor, canvas, handlers, touchOptions) {
305
+ const allowPinchZoom = touchOptions?.pinchToZoom !== false;
306
+ const allowFingerPan = touchOptions?.fingerPanInPenMode !== false;
307
+ const allowTapUndoRedo = touchOptions?.tapUndoRedo !== false;
308
+ const allowTrackpadGestures = touchOptions?.trackpadGestures !== false;
305
309
  const activeTouchPoints = /* @__PURE__ */ new Map();
306
310
  const touchTapState = {
307
311
  active: false,
@@ -343,7 +347,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
343
347
  if (activeTouchPoints.size > 0) return;
344
348
  if (!touchTapState.active) return;
345
349
  const elapsed = performance.now() - touchTapState.startTime;
346
- if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
350
+ if (allowTapUndoRedo && !touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
347
351
  const fingerCount = touchTapState.maxTouchCount;
348
352
  const now = performance.now();
349
353
  const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
@@ -391,9 +395,9 @@ function createTouchInteractionController(editor, canvas, handlers) {
391
395
  const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
392
396
  const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
393
397
  if (touchCameraState.mode === "not-sure") {
394
- if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
398
+ if (allowPinchZoom && touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
395
399
  else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
396
- } else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
400
+ } else if (allowPinchZoom && touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
397
401
  const canvasRect = canvas.getBoundingClientRect();
398
402
  const centerOnCanvasX = center.x - canvasRect.left;
399
403
  const centerOnCanvasY = center.y - canvasRect.top;
@@ -434,7 +438,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
434
438
  beginTouchCameraGesture();
435
439
  return true;
436
440
  }
437
- if (handlers.isPenModeActive() && activeTouchPoints.size === 1) {
441
+ if (allowFingerPan && handlers.isPenModeActive() && activeTouchPoints.size === 1) {
438
442
  handlers.cancelActivePointerInteraction();
439
443
  fingerPanPointerId = event.pointerId;
440
444
  fingerPanSession = beginCameraPan(editor.viewport, event.clientX, event.clientY);
@@ -469,11 +473,15 @@ function createTouchInteractionController(editor, canvas, handlers) {
469
473
  if (wasFingerPan) {
470
474
  endFingerPan();
471
475
  if (releasedPanSession) {
472
- fingerPanSlide = startCameraSlide(
473
- releasedPanSession,
474
- (dx, dy) => editor.panBy(dx, dy),
475
- () => handlers.refreshView()
476
- );
476
+ const slideConfig = handlers.getSlideOptions();
477
+ if (slideConfig.enabled) {
478
+ fingerPanSlide = startCameraSlide(
479
+ releasedPanSession,
480
+ (dx, dy) => editor.panBy(dx, dy),
481
+ () => handlers.refreshView(),
482
+ slideConfig.slideOptions
483
+ );
484
+ }
477
485
  }
478
486
  }
479
487
  maybeHandleTouchTapGesture();
@@ -484,6 +492,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
484
492
  const handleGestureEvent = (event, container) => {
485
493
  if (!container.contains(event.target)) return;
486
494
  event.preventDefault();
495
+ if (!allowTrackpadGestures) return;
487
496
  const gestureEvent = event;
488
497
  if (gestureEvent.scale == null) return;
489
498
  if (event.type === "gesturestart") {
@@ -527,7 +536,7 @@ function createTouchInteractionController(editor, canvas, handlers) {
527
536
  }
528
537
 
529
538
  // src/canvas/keyboardShortcuts.ts
530
- var TOOL_SHORTCUTS = {
539
+ var DEFAULT_TOOL_SHORTCUTS = {
531
540
  v: "select",
532
541
  h: "hand",
533
542
  e: "eraser",
@@ -539,6 +548,11 @@ var TOOL_SHORTCUTS = {
539
548
  o: "circle",
540
549
  c: "circle"
541
550
  };
551
+ function resolveToolShortcuts(shortcutOptions) {
552
+ if (!shortcutOptions?.toolShortcuts) return DEFAULT_TOOL_SHORTCUTS;
553
+ if (shortcutOptions.overrideDefaults) return { ...shortcutOptions.toolShortcuts };
554
+ return { ...DEFAULT_TOOL_SHORTCUTS, ...shortcutOptions.toolShortcuts };
555
+ }
542
556
  function isEditableTarget(eventTarget) {
543
557
  const element = eventTarget;
544
558
  if (!element) return false;
@@ -546,7 +560,7 @@ function isEditableTarget(eventTarget) {
546
560
  const tagName = element.tagName;
547
561
  return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
548
562
  }
549
- function handleKeyboardShortcutKeyDown(event, handlers) {
563
+ function handleKeyboardShortcutKeyDown(event, handlers, toolShortcutMap = DEFAULT_TOOL_SHORTCUTS) {
550
564
  if (isEditableTarget(event.target)) return;
551
565
  const loweredKey = event.key.toLowerCase();
552
566
  const isMetaPressed = event.metaKey || event.ctrlKey;
@@ -559,7 +573,7 @@ function handleKeyboardShortcutKeyDown(event, handlers) {
559
573
  }
560
574
  }
561
575
  if (!isMetaPressed && !event.altKey) {
562
- const nextToolId = TOOL_SHORTCUTS[loweredKey];
576
+ const nextToolId = toolShortcutMap[loweredKey];
563
577
  if (nextToolId && handlers.isToolAvailable(nextToolId)) {
564
578
  handlers.setToolFromShortcut(nextToolId);
565
579
  event.preventDefault();
@@ -734,8 +748,17 @@ function getHandlePagePoint(bounds, handle) {
734
748
  }
735
749
  }
736
750
  var ZOOM_WHEEL_CAP = 10;
751
+ var VIEW_ONLY_TOOLS = /* @__PURE__ */ new Set(["select", "hand"]);
737
752
  function useTsdrawCanvasController(options = {}) {
738
753
  const onMountRef = useRef(options.onMount);
754
+ const onChangeRef = useRef(options.onChange);
755
+ const onCameraChangeRef = useRef(options.onCameraChange);
756
+ const onToolChangeRef = useRef(options.onToolChange);
757
+ const cameraOptionsRef = useRef(options.cameraOptions);
758
+ const touchOptionsRef = useRef(options.touchOptions);
759
+ const keyboardShortcutsRef = useRef(options.keyboardShortcuts);
760
+ const penOptionsRef = useRef(options.penOptions);
761
+ const readOnlyRef = useRef(options.readOnly ?? false);
739
762
  const containerRef = useRef(null);
740
763
  const canvasRef = useRef(null);
741
764
  const editorRef = useRef(null);
@@ -795,6 +818,30 @@ function useTsdrawCanvasController(options = {}) {
795
818
  useEffect(() => {
796
819
  onMountRef.current = options.onMount;
797
820
  }, [options.onMount]);
821
+ useEffect(() => {
822
+ onChangeRef.current = options.onChange;
823
+ }, [options.onChange]);
824
+ useEffect(() => {
825
+ onCameraChangeRef.current = options.onCameraChange;
826
+ }, [options.onCameraChange]);
827
+ useEffect(() => {
828
+ onToolChangeRef.current = options.onToolChange;
829
+ }, [options.onToolChange]);
830
+ useEffect(() => {
831
+ cameraOptionsRef.current = options.cameraOptions;
832
+ }, [options.cameraOptions]);
833
+ useEffect(() => {
834
+ touchOptionsRef.current = options.touchOptions;
835
+ }, [options.touchOptions]);
836
+ useEffect(() => {
837
+ keyboardShortcutsRef.current = options.keyboardShortcuts;
838
+ }, [options.keyboardShortcuts]);
839
+ useEffect(() => {
840
+ penOptionsRef.current = options.penOptions;
841
+ }, [options.penOptions]);
842
+ useEffect(() => {
843
+ readOnlyRef.current = options.readOnly ?? false;
844
+ }, [options.readOnly]);
798
845
  useEffect(() => {
799
846
  selectedShapeIdsRef.current = selectedShapeIds;
800
847
  }, [selectedShapeIds]);
@@ -918,14 +965,21 @@ function useTsdrawCanvasController(options = {}) {
918
965
  const canvas = canvasRef.current;
919
966
  if (!container || !canvas) return;
920
967
  const initialTool = options.initialTool ?? "pen";
968
+ const cameraOpts = cameraOptionsRef.current;
969
+ const touchOpts = touchOptionsRef.current;
970
+ const toolShortcutMap = resolveToolShortcuts(keyboardShortcutsRef.current);
921
971
  const editor = new Editor({
922
972
  toolDefinitions: options.toolDefinitions,
923
- initialToolId: initialTool
973
+ initialToolId: initialTool,
974
+ zoomRange: cameraOpts?.zoomRange
924
975
  });
925
976
  editor.renderer.setTheme(options.theme ?? "light");
926
977
  if (!editor.tools.hasTool(initialTool)) {
927
978
  editor.setCurrentTool("pen");
928
979
  }
980
+ if (options.snapshot) {
981
+ editor.loadPersistenceSnapshot(options.snapshot);
982
+ }
929
983
  let disposed = false;
930
984
  let ignorePersistenceChanges = false;
931
985
  let disposeMount;
@@ -1092,16 +1146,24 @@ function useTsdrawCanvasController(options = {}) {
1092
1146
  render();
1093
1147
  refreshSelectionBounds(editor);
1094
1148
  };
1149
+ const emitCameraChange = () => {
1150
+ onCameraChangeRef.current?.({ ...editor.viewport });
1151
+ };
1095
1152
  const touchInteractions = createTouchInteractionController(editor, canvas, {
1096
1153
  cancelActivePointerInteraction,
1097
1154
  refreshView: () => {
1098
1155
  render();
1099
1156
  refreshSelectionBounds(editor);
1157
+ emitCameraChange();
1100
1158
  },
1101
1159
  runUndo: () => applyDocumentChangeResult(editor.undo()),
1102
1160
  runRedo: () => applyDocumentChangeResult(editor.redo()),
1103
- isPenModeActive: () => penModeRef.current
1104
- });
1161
+ isPenModeActive: () => penModeRef.current,
1162
+ getSlideOptions: () => ({
1163
+ enabled: cameraOptionsRef.current?.slideEnabled !== false,
1164
+ slideOptions: { friction: cameraOptionsRef.current?.slideFriction }
1165
+ })
1166
+ }, touchOpts);
1105
1167
  const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
1106
1168
  const stopActiveSlide = () => {
1107
1169
  if (activeCameraSlideRef.current) {
@@ -1111,8 +1173,10 @@ function useTsdrawCanvasController(options = {}) {
1111
1173
  };
1112
1174
  const handlePointerDown = (e) => {
1113
1175
  if (!canvas.contains(e.target)) return;
1176
+ if (cameraOptionsRef.current?.locked && e.pointerType !== "pen") return;
1114
1177
  stopActiveSlide();
1115
- if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1178
+ const penAutoDetect = penOptionsRef.current?.autoDetect !== false;
1179
+ if (penAutoDetect && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1116
1180
  penDetectedRef.current = true;
1117
1181
  penModeRef.current = true;
1118
1182
  }
@@ -1133,6 +1197,9 @@ function useTsdrawCanvasController(options = {}) {
1133
1197
  if (activePointerIdsRef.current.size > 1) {
1134
1198
  return;
1135
1199
  }
1200
+ if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(currentToolRef.current)) {
1201
+ return;
1202
+ }
1136
1203
  isPointerActiveRef.current = true;
1137
1204
  editor.beginHistoryEntry();
1138
1205
  canvas.setPointerCapture(e.pointerId);
@@ -1140,7 +1207,8 @@ function useTsdrawCanvasController(options = {}) {
1140
1207
  updatePointerPreview(e.clientX, e.clientY);
1141
1208
  const first = sampleEvents(e)[0];
1142
1209
  const { x, y } = getPagePoint(first);
1143
- const pressure = first.pressure ?? 0.5;
1210
+ const pressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
1211
+ const pressure = (first.pressure ?? 0.5) * pressureSensitivity;
1144
1212
  const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
1145
1213
  if (currentToolRef.current === "select") {
1146
1214
  const hit = getTopShapeAtPoint(editor, { x, y });
@@ -1187,7 +1255,8 @@ function useTsdrawCanvasController(options = {}) {
1187
1255
  refreshSelectionBounds(editor);
1188
1256
  };
1189
1257
  const handlePointerMove = (e) => {
1190
- if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1258
+ const penAutoDetectOnMove = penOptionsRef.current?.autoDetect !== false;
1259
+ if (penAutoDetectOnMove && !penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1191
1260
  penDetectedRef.current = true;
1192
1261
  penModeRef.current = true;
1193
1262
  }
@@ -1202,9 +1271,10 @@ function useTsdrawCanvasController(options = {}) {
1202
1271
  const dx = prevClient ? e.clientX - prevClient.x : 0;
1203
1272
  const dy = prevClient ? e.clientY - prevClient.y : 0;
1204
1273
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
1274
+ const movePressureSensitivity = penOptionsRef.current?.pressureSensitivity ?? 1;
1205
1275
  for (const sample of sampleEvents(e)) {
1206
1276
  const { x, y } = getPagePoint(sample);
1207
- const pressure = sample.pressure ?? 0.5;
1277
+ const pressure = (sample.pressure ?? 0.5) * movePressureSensitivity;
1208
1278
  const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
1209
1279
  editor.input.pointerMove(x, y, pressure, isPen);
1210
1280
  }
@@ -1338,14 +1408,18 @@ function useTsdrawCanvasController(options = {}) {
1338
1408
  editor.tools.pointerUp();
1339
1409
  render();
1340
1410
  refreshSelectionBounds(editor);
1341
- if (handPanSession) {
1411
+ if (handPanSession && cameraOptionsRef.current?.slideEnabled !== false) {
1342
1412
  activeCameraSlideRef.current = startCameraSlide(
1343
1413
  handPanSession,
1344
- (slideDx, slideDy) => editor.panBy(slideDx, slideDy),
1414
+ (slideDx, slideDy) => {
1415
+ editor.panBy(slideDx, slideDy);
1416
+ emitCameraChange();
1417
+ },
1345
1418
  () => {
1346
1419
  render();
1347
1420
  refreshSelectionBounds(editor);
1348
- }
1421
+ },
1422
+ { friction: cameraOptionsRef.current?.slideFriction }
1349
1423
  );
1350
1424
  }
1351
1425
  if (pendingRemoteDocumentRef.current) {
@@ -1390,41 +1464,59 @@ function useTsdrawCanvasController(options = {}) {
1390
1464
  const handleWheel = (e) => {
1391
1465
  if (!container.contains(e.target)) return;
1392
1466
  e.preventDefault();
1467
+ const camOpts = cameraOptionsRef.current;
1468
+ if (camOpts?.locked) return;
1469
+ if (camOpts?.wheelBehavior === "none") return;
1393
1470
  if (touchInteractions.isTrackpadZoomActive()) return;
1394
1471
  const delta = normalizeWheelDelta(e);
1472
+ const panMultiplier = camOpts?.panSpeed ?? 1;
1473
+ const zoomMultiplier = camOpts?.zoomSpeed ?? 1;
1395
1474
  if (delta.z !== 0) {
1396
1475
  const rect = canvas.getBoundingClientRect();
1397
1476
  const pointX = e.clientX - rect.left;
1398
1477
  const pointY = e.clientY - rect.top;
1399
- editor.zoomAt(Math.exp(delta.z), pointX, pointY);
1478
+ editor.zoomAt(Math.exp(delta.z * zoomMultiplier), pointX, pointY);
1400
1479
  } else {
1401
- editor.panBy(delta.x, delta.y);
1480
+ editor.panBy(delta.x * panMultiplier, delta.y * panMultiplier);
1402
1481
  }
1403
1482
  render();
1404
1483
  refreshSelectionBounds(editor);
1484
+ emitCameraChange();
1405
1485
  };
1406
1486
  const handleGestureEvent = (e) => {
1407
1487
  touchInteractions.handleGestureEvent(e, container);
1408
1488
  };
1409
1489
  const handleKeyDown = (e) => {
1490
+ if (keyboardShortcutsRef.current?.enabled === false) return;
1491
+ const isReadOnly = readOnlyRef.current;
1410
1492
  handleKeyboardShortcutKeyDown(e, {
1411
- isToolAvailable: (tool) => editor.tools.hasTool(tool),
1493
+ isToolAvailable: (tool) => {
1494
+ if (isReadOnly && !VIEW_ONLY_TOOLS.has(tool)) return false;
1495
+ return editor.tools.hasTool(tool);
1496
+ },
1412
1497
  setToolFromShortcut: (tool) => {
1413
1498
  editor.setCurrentTool(tool);
1414
1499
  setCurrentToolState(tool);
1415
1500
  currentToolRef.current = tool;
1416
1501
  if (tool !== "select") resetSelectUi();
1417
1502
  render();
1503
+ onToolChangeRef.current?.(tool);
1504
+ },
1505
+ runHistoryShortcut: (shouldRedo) => {
1506
+ if (isReadOnly) return false;
1507
+ return applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo());
1508
+ },
1509
+ deleteSelection: () => {
1510
+ if (isReadOnly) return false;
1511
+ return currentToolRef.current === "select" ? deleteCurrentSelection() : false;
1418
1512
  },
1419
- runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
1420
- deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
1421
1513
  dispatchKeyDown: (event) => {
1422
1514
  editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1423
1515
  editor.tools.keyDown({ key: event.key });
1424
1516
  render();
1425
1517
  },
1426
1518
  dispatchKeyUp: () => void 0
1427
- });
1519
+ }, toolShortcutMap);
1428
1520
  };
1429
1521
  const handleKeyUp = (e) => {
1430
1522
  handleKeyboardShortcutKeyUp(e, {
@@ -1505,6 +1597,7 @@ function useTsdrawCanvasController(options = {}) {
1505
1597
  const cleanupEditorListener = editor.listen(() => {
1506
1598
  if (ignorePersistenceChanges) return;
1507
1599
  schedulePersist();
1600
+ onChangeRef.current?.(editor.getDocumentSnapshot());
1508
1601
  });
1509
1602
  const cleanupHistoryListener = editor.listenHistory(() => {
1510
1603
  syncHistoryState();
@@ -1571,6 +1664,9 @@ function useTsdrawCanvasController(options = {}) {
1571
1664
  render();
1572
1665
  }
1573
1666
  });
1667
+ if (options.autoFocus !== false) {
1668
+ container.focus({ preventScroll: true });
1669
+ }
1574
1670
  return () => {
1575
1671
  disposed = true;
1576
1672
  schedulePersistRef.current = null;
@@ -1619,10 +1715,12 @@ function useTsdrawCanvasController(options = {}) {
1619
1715
  const editor = editorRef.current;
1620
1716
  if (!editor) return;
1621
1717
  if (!editor.tools.hasTool(tool)) return;
1718
+ if (readOnlyRef.current && !VIEW_ONLY_TOOLS.has(tool)) return;
1622
1719
  editor.setCurrentTool(tool);
1623
1720
  setCurrentToolState(tool);
1624
1721
  currentToolRef.current = tool;
1625
1722
  if (tool !== "select") resetSelectUi();
1723
+ onToolChangeRef.current?.(tool);
1626
1724
  },
1627
1725
  [resetSelectUi]
1628
1726
  );
@@ -1858,12 +1956,22 @@ function Tsdraw(props) {
1858
1956
  initialTool,
1859
1957
  theme: resolvedTheme,
1860
1958
  persistenceKey: props.persistenceKey,
1861
- onMount: props.onMount
1959
+ onMount: props.onMount,
1960
+ cameraOptions: props.cameraOptions,
1961
+ touchOptions: props.touchOptions,
1962
+ keyboardShortcuts: props.keyboardShortcuts,
1963
+ penOptions: props.penOptions,
1964
+ readOnly: props.readOnly,
1965
+ autoFocus: props.autoFocus,
1966
+ snapshot: props.snapshot,
1967
+ onChange: props.onChange,
1968
+ onCameraChange: props.onCameraChange,
1969
+ onToolChange: props.onToolChange
1862
1970
  });
1863
1971
  const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
1864
1972
  const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
1865
1973
  const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
1866
- const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
1974
+ const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true || props.readOnly === true;
1867
1975
  const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
1868
1976
  const defaultToolOverlay = /* @__PURE__ */ jsx(
1869
1977
  ToolOverlay,
@@ -1952,12 +2060,14 @@ function Tsdraw(props) {
1952
2060
  "div",
1953
2061
  {
1954
2062
  ref: containerRef,
2063
+ tabIndex: 0,
1955
2064
  className: `tsdraw tsdraw-${resolvedTheme}mode ${props.className ?? ""}`,
1956
2065
  style: {
1957
2066
  width: props.width ?? "100%",
1958
2067
  height: props.height ?? "100%",
1959
2068
  position: "relative",
1960
2069
  overflow: "hidden",
2070
+ outline: "none",
1961
2071
  ...props.style
1962
2072
  },
1963
2073
  children: [