@canvas-harness/react 0.0.1 → 0.0.3

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
@@ -116,6 +116,21 @@ type CanvasProps = {
116
116
  * <Canvas background={{ color: '#fffaf3', pattern: 'dots', gap: 24 }} />
117
117
  */
118
118
  background?: CanvasBackground;
119
+ /**
120
+ * Color for all selection chrome: outline, resize + rotate handles,
121
+ * edge endpoint + midpoint handles, marquee, drag-create preview,
122
+ * and the draft edge during creation. Defaults to `#3b82f6`. Update
123
+ * by changing the prop — `<Canvas>` calls
124
+ * `renderer.setSelectionColor` without recreating the renderer.
125
+ *
126
+ * Accepts any CSS color literal (hex, rgb(), named). Typically you
127
+ * also want to pass the same value to `<Minimap viewportColor={...} />`
128
+ * so the two stay visually in sync.
129
+ *
130
+ * @example
131
+ * <Canvas selectionColor="#10b981" />
132
+ */
133
+ selectionColor?: string;
119
134
  /**
120
135
  * Render a custom node's React subtree. Called once per
121
136
  * library-mounted custom-node id; positioning is handled by the
@@ -449,7 +464,7 @@ declare function useCanRedo(): boolean;
449
464
  * zoom; this hook handles primary-button gestures only.
450
465
  */
451
466
 
452
- type InteractionTool = 'select' | 'rect' | 'ellipse' | 'diamond' | 'capsule' | 'arrow' | 'text';
467
+ type InteractionTool = 'select' | 'pan' | 'rect' | 'ellipse' | 'diamond' | 'capsule' | 'arrow' | 'text';
453
468
 
454
469
  /**
455
470
  * @canvas-harness/react
package/dist/index.d.ts CHANGED
@@ -116,6 +116,21 @@ type CanvasProps = {
116
116
  * <Canvas background={{ color: '#fffaf3', pattern: 'dots', gap: 24 }} />
117
117
  */
118
118
  background?: CanvasBackground;
119
+ /**
120
+ * Color for all selection chrome: outline, resize + rotate handles,
121
+ * edge endpoint + midpoint handles, marquee, drag-create preview,
122
+ * and the draft edge during creation. Defaults to `#3b82f6`. Update
123
+ * by changing the prop — `<Canvas>` calls
124
+ * `renderer.setSelectionColor` without recreating the renderer.
125
+ *
126
+ * Accepts any CSS color literal (hex, rgb(), named). Typically you
127
+ * also want to pass the same value to `<Minimap viewportColor={...} />`
128
+ * so the two stay visually in sync.
129
+ *
130
+ * @example
131
+ * <Canvas selectionColor="#10b981" />
132
+ */
133
+ selectionColor?: string;
119
134
  /**
120
135
  * Render a custom node's React subtree. Called once per
121
136
  * library-mounted custom-node id; positioning is handled by the
@@ -449,7 +464,7 @@ declare function useCanRedo(): boolean;
449
464
  * zoom; this hook handles primary-button gestures only.
450
465
  */
451
466
 
452
- type InteractionTool = 'select' | 'rect' | 'ellipse' | 'diamond' | 'capsule' | 'arrow' | 'text';
467
+ type InteractionTool = 'select' | 'pan' | 'rect' | 'ellipse' | 'diamond' | 'capsule' | 'arrow' | 'text';
453
468
 
454
469
  /**
455
470
  * @canvas-harness/react
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRenderer, DEFAULT_MINIMAP_MAX_NODES, renderMinimapContent, sceneBounds, drawMinimapViewport, worldViewportFromCamera, screenToWorld, hitTestAny, copy, cut, paste, createPalmRejectionState, createDefaultTextareaEditor, minimapScreenToWorld, notePenActive, shouldRejectTouch, notePenInactive, asEdgeId, midpointToCubicControls, projectToNodeBoundary, marqueeNodes, shouldAutoFit, computeAutoFitHeight, hitTestPoint, worldToNodeLocal, edgeLabelBoundsWorld, asNodeId, zoomAtScreenPoint, clampZoom, panByScreen } from '@canvas-harness/core';
2
- import { createContext, useContext, useRef, useState, useEffect, useSyncExternalStore } from 'react';
2
+ import { createContext, useContext, useSyncExternalStore, useRef, useState, useEffect } from 'react';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
 
5
5
  // src/Canvas.tsx
@@ -16,6 +16,54 @@ function useCanvasStore() {
16
16
  }
17
17
  return store;
18
18
  }
19
+ function useInteractionState() {
20
+ const store = useCanvasStore();
21
+ return useSyncExternalStore(
22
+ (cb) => store.subscribe("interaction", cb),
23
+ () => store.getInteractionState()
24
+ );
25
+ }
26
+ function useInteractionMode() {
27
+ const store = useCanvasStore();
28
+ return useSyncExternalStore(
29
+ (cb) => {
30
+ let lastMode = store.getInteractionState().mode;
31
+ return store.subscribe("interaction", (state) => {
32
+ if (state.mode !== lastMode) {
33
+ lastMode = state.mode;
34
+ cb();
35
+ }
36
+ });
37
+ },
38
+ () => store.getInteractionState().mode
39
+ );
40
+ }
41
+ function useCursor() {
42
+ const store = useCanvasStore();
43
+ return useSyncExternalStore(
44
+ (cb) => store.subscribe("interaction", cb),
45
+ () => store.getInteractionState().pointer
46
+ );
47
+ }
48
+ function useIsMoving() {
49
+ const mode = useInteractionMode();
50
+ return mode === "panning" || mode === "zooming" || mode === "dragging" || mode === "resizing" || mode === "rotating";
51
+ }
52
+ var EMPTY_DRAGGED = [];
53
+ function useDraggedIds() {
54
+ const store = useCanvasStore();
55
+ return useSyncExternalStore(
56
+ (cb) => store.subscribe("interaction", cb),
57
+ () => {
58
+ const state = store.getInteractionState();
59
+ return state.draggedIds.length === 0 ? EMPTY_DRAGGED : state.draggedIds;
60
+ }
61
+ );
62
+ }
63
+ function useIsPenActive() {
64
+ const cursor = useCursor();
65
+ return cursor?.pointerType === "pen";
66
+ }
19
67
  function EditorMount({
20
68
  store,
21
69
  factory = createDefaultTextareaEditor
@@ -711,7 +759,9 @@ var useOverlayHost = () => {
711
759
  }, []);
712
760
  return { mountedIds, setMountedIds };
713
761
  };
714
- var usePanZoom = (ref, store) => {
762
+ var usePanZoom = (ref, store, tool) => {
763
+ const toolRef = useRef(tool);
764
+ toolRef.current = tool;
715
765
  useEffect(() => {
716
766
  const el = ref.current;
717
767
  if (!el) return;
@@ -849,7 +899,8 @@ var usePanZoom = (ref, store) => {
849
899
  }
850
900
  return;
851
901
  }
852
- if (e.button === 1 || e.button === 0 && panActivatedBySpace) {
902
+ const handToolActive = toolRef.current === "pan";
903
+ if (e.button === 1 || e.button === 0 && (panActivatedBySpace || handToolActive)) {
853
904
  panning = true;
854
905
  lastX = e.clientX;
855
906
  lastY = e.clientY;
@@ -979,6 +1030,7 @@ function CanvasSurface({
979
1030
  onCreateDrag,
980
1031
  arrowDefaults,
981
1032
  background,
1033
+ selectionColor,
982
1034
  renderCustomNodeView,
983
1035
  children
984
1036
  }) {
@@ -991,8 +1043,9 @@ function CanvasSurface({
991
1043
  const toolRef = useRef(tool);
992
1044
  toolRef.current = tool;
993
1045
  const { w, h } = useResizeObserver(wrapRef);
994
- usePanZoom(wrapRef, store);
1046
+ usePanZoom(wrapRef, store, tool);
995
1047
  useInteractionGesture(wrapRef, store, tool);
1048
+ const interactionMode = useInteractionMode();
996
1049
  useArrowTool(wrapRef, store, tool === "arrow", arrowDefaults);
997
1050
  const { mountedIds, setMountedIds } = useOverlayHost();
998
1051
  const [camera, setCamera] = useState(() => store.getCamera());
@@ -1011,6 +1064,7 @@ function CanvasSurface({
1011
1064
  width: w,
1012
1065
  height: h,
1013
1066
  background,
1067
+ selectionColor,
1014
1068
  onOverlayChange: (ids) => setMountedIds(ids)
1015
1069
  });
1016
1070
  r.start();
@@ -1024,6 +1078,9 @@ function CanvasSurface({
1024
1078
  useEffect(() => {
1025
1079
  rendererRef.current?.setBackground(background);
1026
1080
  }, [background]);
1081
+ useEffect(() => {
1082
+ if (selectionColor !== void 0) rendererRef.current?.setSelectionColor(selectionColor);
1083
+ }, [selectionColor]);
1027
1084
  useEffect(() => {
1028
1085
  const el = wrapRef.current;
1029
1086
  if (!el) return;
@@ -1061,6 +1118,7 @@ function CanvasSurface({
1061
1118
  el.removeEventListener("dblclick", onDoubleClickHandler);
1062
1119
  };
1063
1120
  }, [store, onClick, onDoubleClick]);
1121
+ const justCommittedRef = useRef(false);
1064
1122
  useEffect(() => {
1065
1123
  const el = wrapRef.current;
1066
1124
  if (!el || !onCreateDrag) return;
@@ -1068,7 +1126,6 @@ function CanvasSurface({
1068
1126
  let startScreen = null;
1069
1127
  let activePointerId = null;
1070
1128
  let committed = false;
1071
- const justCommittedRef = { current: false };
1072
1129
  const screenFromEvent = (e) => {
1073
1130
  const rect = el.getBoundingClientRect();
1074
1131
  return { x: e.clientX - rect.left, y: e.clientY - rect.top };
@@ -1199,7 +1256,7 @@ function CanvasSurface({
1199
1256
  inset: 0,
1200
1257
  background: "#f8fafc",
1201
1258
  overflow: "hidden",
1202
- cursor: tool === "select" ? "default" : "crosshair",
1259
+ cursor: tool === "pan" ? interactionMode === "panning" ? "grabbing" : "grab" : tool === "select" ? "default" : "crosshair",
1203
1260
  touchAction: "none"
1204
1261
  },
1205
1262
  children: [
@@ -1549,54 +1606,6 @@ function useCamera() {
1549
1606
  () => store.getCamera()
1550
1607
  );
1551
1608
  }
1552
- function useInteractionState() {
1553
- const store = useCanvasStore();
1554
- return useSyncExternalStore(
1555
- (cb) => store.subscribe("interaction", cb),
1556
- () => store.getInteractionState()
1557
- );
1558
- }
1559
- function useInteractionMode() {
1560
- const store = useCanvasStore();
1561
- return useSyncExternalStore(
1562
- (cb) => {
1563
- let lastMode = store.getInteractionState().mode;
1564
- return store.subscribe("interaction", (state) => {
1565
- if (state.mode !== lastMode) {
1566
- lastMode = state.mode;
1567
- cb();
1568
- }
1569
- });
1570
- },
1571
- () => store.getInteractionState().mode
1572
- );
1573
- }
1574
- function useCursor() {
1575
- const store = useCanvasStore();
1576
- return useSyncExternalStore(
1577
- (cb) => store.subscribe("interaction", cb),
1578
- () => store.getInteractionState().pointer
1579
- );
1580
- }
1581
- function useIsMoving() {
1582
- const mode = useInteractionMode();
1583
- return mode === "panning" || mode === "zooming" || mode === "dragging" || mode === "resizing" || mode === "rotating";
1584
- }
1585
- var EMPTY_DRAGGED = [];
1586
- function useDraggedIds() {
1587
- const store = useCanvasStore();
1588
- return useSyncExternalStore(
1589
- (cb) => store.subscribe("interaction", cb),
1590
- () => {
1591
- const state = store.getInteractionState();
1592
- return state.draggedIds.length === 0 ? EMPTY_DRAGGED : state.draggedIds;
1593
- }
1594
- );
1595
- }
1596
- function useIsPenActive() {
1597
- const cursor = useCursor();
1598
- return cursor?.pointerType === "pen";
1599
- }
1600
1609
  function useLocalPresence() {
1601
1610
  const store = useCanvasStore();
1602
1611
  return useSyncExternalStore(