canvu-react 0.4.64 → 0.4.66

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/react.cjs CHANGED
@@ -3223,6 +3223,30 @@ function createToolPlugin(options) {
3223
3223
  }
3224
3224
  });
3225
3225
  }
3226
+
3227
+ // src/react/merge-by-id.ts
3228
+ function mergeById(baseItems, ...contributions) {
3229
+ const next = [...baseItems];
3230
+ const indexById = /* @__PURE__ */ new Map();
3231
+ for (const [index, item] of next.entries()) {
3232
+ if (!indexById.has(item.id)) {
3233
+ indexById.set(item.id, index);
3234
+ }
3235
+ }
3236
+ for (const contribution of contributions) {
3237
+ if (!contribution?.length) continue;
3238
+ for (const item of contribution) {
3239
+ const existingIndex = indexById.get(item.id);
3240
+ if (existingIndex !== void 0) {
3241
+ next[existingIndex] = item;
3242
+ continue;
3243
+ }
3244
+ indexById.set(item.id, next.length);
3245
+ next.push(item);
3246
+ }
3247
+ }
3248
+ return next;
3249
+ }
3226
3250
  var menuStyle = {
3227
3251
  position: "fixed",
3228
3252
  zIndex: 1e4,
@@ -3252,9 +3276,97 @@ var dividerStyle = {
3252
3276
  margin: "4px 8px",
3253
3277
  background: "#e2e8f0"
3254
3278
  };
3279
+ var SHAPE_CONTEXT_MENU_ITEM_IDS = {
3280
+ cut: "cut",
3281
+ copy: "copy",
3282
+ duplicate: "duplicate",
3283
+ reorderDivider: "reorder-divider",
3284
+ bringToFront: "bring-to-front",
3285
+ bringForward: "bring-forward",
3286
+ sendBackward: "send-backward",
3287
+ sendToBack: "send-to-back",
3288
+ lockDivider: "lock-divider",
3289
+ toggleLock: "toggle-lock",
3290
+ delete: "delete"
3291
+ };
3292
+ function renderAction(label, onClick, options) {
3293
+ return /* @__PURE__ */ jsxRuntime.jsx(
3294
+ "button",
3295
+ {
3296
+ type: "button",
3297
+ role: "menuitem",
3298
+ style: {
3299
+ ...itemStyle,
3300
+ ...options?.danger ? { color: "#b91c1c" } : {}
3301
+ },
3302
+ onMouseEnter: (e) => {
3303
+ e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
3304
+ },
3305
+ onMouseLeave: (e) => {
3306
+ e.currentTarget.style.background = "transparent";
3307
+ },
3308
+ onClick,
3309
+ children: label
3310
+ }
3311
+ );
3312
+ }
3313
+ function renderDivider() {
3314
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: dividerStyle });
3315
+ }
3316
+ var DEFAULT_SHAPE_CONTEXT_MENU_ITEMS = [
3317
+ {
3318
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.cut,
3319
+ render: (ctx) => renderAction("Recortar", ctx.cut)
3320
+ },
3321
+ {
3322
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.copy,
3323
+ render: (ctx) => renderAction("Copiar", ctx.copy)
3324
+ },
3325
+ {
3326
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.duplicate,
3327
+ render: (ctx) => renderAction("Duplicar", ctx.duplicate)
3328
+ },
3329
+ {
3330
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.reorderDivider,
3331
+ render: renderDivider
3332
+ },
3333
+ {
3334
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.bringToFront,
3335
+ render: (ctx) => renderAction("Trazer para frente", ctx.bringToFront)
3336
+ },
3337
+ {
3338
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.bringForward,
3339
+ render: (ctx) => renderAction("Avancar uma camada", ctx.bringForward)
3340
+ },
3341
+ {
3342
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.sendBackward,
3343
+ render: (ctx) => renderAction("Recuar uma camada", ctx.sendBackward)
3344
+ },
3345
+ {
3346
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.sendToBack,
3347
+ render: (ctx) => renderAction("Enviar para tras", ctx.sendToBack)
3348
+ },
3349
+ {
3350
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.lockDivider,
3351
+ render: renderDivider
3352
+ },
3353
+ {
3354
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.toggleLock,
3355
+ render: (ctx) => renderAction(
3356
+ ctx.allSelectedLocked ? "Desbloquear" : "Bloquear",
3357
+ ctx.toggleLock
3358
+ )
3359
+ },
3360
+ {
3361
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.delete,
3362
+ render: (ctx) => renderAction("Apagar", ctx.delete, { danger: true })
3363
+ }
3364
+ ];
3255
3365
  function ShapeContextMenu({
3256
3366
  x,
3257
3367
  y,
3368
+ selectedIds,
3369
+ selectedItems = [],
3258
3370
  allSelectedLocked,
3259
3371
  onClose,
3260
3372
  onToggleLock,
@@ -3265,7 +3377,10 @@ function ShapeContextMenu({
3265
3377
  onSendBackward,
3266
3378
  onSendToBack,
3267
3379
  onDuplicate,
3268
- onDelete
3380
+ onDelete,
3381
+ items,
3382
+ itemContributions,
3383
+ children
3269
3384
  }) {
3270
3385
  const rootRef = react.useRef(null);
3271
3386
  react.useLayoutEffect(() => {
@@ -3305,49 +3420,43 @@ function ShapeContextMenu({
3305
3420
  document.removeEventListener("pointerdown", onPointerDown, true);
3306
3421
  };
3307
3422
  }, [onClose]);
3423
+ const menuItems = react.useMemo(
3424
+ () => mergeById(items ?? DEFAULT_SHAPE_CONTEXT_MENU_ITEMS, itemContributions),
3425
+ [itemContributions, items]
3426
+ );
3308
3427
  const run = (fn) => () => {
3309
3428
  fn();
3310
3429
  onClose();
3311
3430
  };
3312
- const renderAction = (label, onClick, options) => /* @__PURE__ */ jsxRuntime.jsx(
3313
- "button",
3314
- {
3315
- type: "button",
3316
- role: "menuitem",
3317
- style: {
3318
- ...itemStyle,
3319
- ...options?.danger ? { color: "#b91c1c" } : {}
3320
- },
3321
- onMouseEnter: (e) => {
3322
- e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
3323
- },
3324
- onMouseLeave: (e) => {
3325
- e.currentTarget.style.background = "transparent";
3326
- },
3327
- onClick: run(onClick),
3328
- children: label
3329
- }
3330
- );
3331
- const menu = /* @__PURE__ */ jsxRuntime.jsxs(
3431
+ const resolvedSelectedIds = selectedIds ?? selectedItems.map((item) => item.id);
3432
+ const resolvedAllSelectedLocked = allSelectedLocked ?? (selectedItems.length > 0 && selectedItems.every((item) => item.locked));
3433
+ const menuContext = {
3434
+ position: { x, y },
3435
+ selectedIds: resolvedSelectedIds,
3436
+ selectedItems,
3437
+ allSelectedLocked: resolvedAllSelectedLocked,
3438
+ anySelectedLocked: selectedItems.some((item) => item.locked),
3439
+ close: onClose,
3440
+ cut: run(onCut),
3441
+ copy: run(onCopy),
3442
+ duplicate: run(onDuplicate),
3443
+ toggleLock: run(onToggleLock),
3444
+ bringToFront: run(onBringToFront),
3445
+ bringForward: run(onBringForward),
3446
+ sendBackward: run(onSendBackward),
3447
+ sendToBack: run(onSendToBack),
3448
+ delete: run(onDelete),
3449
+ deleteSelection: run(onDelete)
3450
+ };
3451
+ const content = typeof children === "function" ? children(menuContext) : children !== void 0 ? children : menuItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: item.render(menuContext) }, item.id));
3452
+ const menu = /* @__PURE__ */ jsxRuntime.jsx(
3332
3453
  "div",
3333
3454
  {
3334
3455
  ref: rootRef,
3335
3456
  "data-slot": "shape-context-menu",
3336
3457
  style: { ...menuStyle, left: x, top: y },
3337
3458
  role: "menu",
3338
- children: [
3339
- renderAction("Recortar", onCut),
3340
- renderAction("Copiar", onCopy),
3341
- renderAction("Duplicar", onDuplicate),
3342
- /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: dividerStyle }),
3343
- renderAction("Trazer para frente", onBringToFront),
3344
- renderAction("Avancar uma camada", onBringForward),
3345
- renderAction("Recuar uma camada", onSendBackward),
3346
- renderAction("Enviar para tras", onSendToBack),
3347
- /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": true, style: dividerStyle }),
3348
- renderAction(allSelectedLocked ? "Desbloquear" : "Bloquear", onToggleLock),
3349
- renderAction("Apagar", onDelete, { danger: true })
3350
- ]
3459
+ children: content
3351
3460
  }
3352
3461
  );
3353
3462
  if (typeof document === "undefined") {
@@ -5062,6 +5171,7 @@ function VectorToolbarComponent({
5062
5171
  value,
5063
5172
  onChange,
5064
5173
  tools,
5174
+ toolContributions,
5065
5175
  overflowToolIds = DEFAULT_OVERFLOW_TOOL_IDS,
5066
5176
  overflowMenuAriaLabel = "More tools",
5067
5177
  "aria-label": ariaLabel = "Canvas tools",
@@ -5079,7 +5189,10 @@ function VectorToolbarComponent({
5079
5189
  }) {
5080
5190
  const pluginContext = react.useContext(CanvuPluginContext);
5081
5191
  const runtimeTools = pluginContext?.resolvedTools;
5082
- const resolvedTools = tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS;
5192
+ const resolvedTools = react.useMemo(
5193
+ () => mergeById(tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS, toolContributions),
5194
+ [runtimeTools, toolContributions, tools]
5195
+ );
5083
5196
  const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
5084
5197
  resolvedTools,
5085
5198
  overflowToolIds
@@ -5653,7 +5766,7 @@ function attachViewportInput(options) {
5653
5766
  if (touchMomentum) {
5654
5767
  touchMomentum.cancel();
5655
5768
  }
5656
- const panOk = allowPrimaryPointerPan();
5769
+ const panOk = allowPrimaryPointerPan(e);
5657
5770
  if (e.pointerType === "mouse" && e.button === 0) {
5658
5771
  if (!panOk) {
5659
5772
  return;
@@ -7677,6 +7790,93 @@ function PresenceRemoteLayer({
7677
7790
  );
7678
7791
  }
7679
7792
 
7793
+ // src/react/read-only-activation.ts
7794
+ function findReadOnlyItemClickPlacement(item, placements) {
7795
+ const toolId = item.customToolId;
7796
+ if (!toolId) return null;
7797
+ return [...placements].reverse().find(
7798
+ (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
7799
+ ) ?? null;
7800
+ }
7801
+ function resolveReadOnlyActivationTarget(input) {
7802
+ const {
7803
+ pointer,
7804
+ camera,
7805
+ container,
7806
+ items,
7807
+ detailItems = items,
7808
+ placements,
7809
+ scope,
7810
+ selectedIds
7811
+ } = input;
7812
+ const rect = container.getBoundingClientRect();
7813
+ const world = camera.screenToWorld(
7814
+ pointer.clientX - rect.left,
7815
+ pointer.clientY - rect.top
7816
+ );
7817
+ const hit = hitTestWorldPoint(items, world.worldX, world.worldY, {
7818
+ lineHitWorld: 10 / camera.zoom,
7819
+ ignoreLocked: true
7820
+ });
7821
+ if (!hit) return null;
7822
+ const customPlacement = findReadOnlyItemClickPlacement(hit, placements);
7823
+ if (customPlacement?.onSelectModeItemClick) {
7824
+ return {
7825
+ item: hit,
7826
+ activation: "custom",
7827
+ worldX: world.worldX,
7828
+ worldY: world.worldY
7829
+ };
7830
+ }
7831
+ if (scope === "all") {
7832
+ return {
7833
+ item: hit,
7834
+ activation: "read-only",
7835
+ worldX: world.worldX,
7836
+ worldY: world.worldY
7837
+ };
7838
+ }
7839
+ if (typeof scope === "function") {
7840
+ const allowed = scope({
7841
+ item: hit,
7842
+ worldX: world.worldX,
7843
+ worldY: world.worldY,
7844
+ clientX: pointer.clientX,
7845
+ clientY: pointer.clientY,
7846
+ pointerType: pointer.pointerType,
7847
+ shiftKey: pointer.shiftKey,
7848
+ altKey: pointer.altKey,
7849
+ metaKey: pointer.metaKey,
7850
+ ctrlKey: pointer.ctrlKey,
7851
+ items: detailItems,
7852
+ selectedIds
7853
+ });
7854
+ if (allowed) {
7855
+ return {
7856
+ item: hit,
7857
+ activation: "read-only",
7858
+ worldX: world.worldX,
7859
+ worldY: world.worldY
7860
+ };
7861
+ }
7862
+ }
7863
+ return null;
7864
+ }
7865
+ function createReadOnlyActivationSession(target, pointer) {
7866
+ return {
7867
+ pointerId: pointer.pointerId,
7868
+ itemId: target.item.id,
7869
+ activation: target.activation,
7870
+ startWorld: { x: target.worldX, y: target.worldY },
7871
+ startScreen: { x: pointer.clientX, y: pointer.clientY }
7872
+ };
7873
+ }
7874
+ function didReadOnlyActivationMovePastTap(session, pointer, tapPx) {
7875
+ const dx = pointer.clientX - session.startScreen.x;
7876
+ const dy = pointer.clientY - session.startScreen.y;
7877
+ return Math.hypot(dx, dy) > tapPx;
7878
+ }
7879
+
7680
7880
  // src/react/stable-selection.ts
7681
7881
  function shallowEqualStringArray(a, b) {
7682
7882
  if (a === b) return true;
@@ -8030,25 +8230,6 @@ function isDefaultMarkerToolStyle(style) {
8030
8230
  function tagCustomPlacementItem(item, toolId) {
8031
8231
  return item.customToolId === toolId ? item : { ...item, customToolId: toolId };
8032
8232
  }
8033
- function findSelectModeItemClickPlacement(item, placements) {
8034
- const toolId = item.customToolId;
8035
- if (!toolId) return null;
8036
- return [...placements].reverse().find(
8037
- (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
8038
- ) ?? null;
8039
- }
8040
- function mergeToolListById(baseTools, pluginTools) {
8041
- const next = [...baseTools];
8042
- for (const tool of pluginTools) {
8043
- const index = next.findIndex((candidate) => candidate.id === tool.id);
8044
- if (index >= 0) {
8045
- next[index] = tool;
8046
- continue;
8047
- }
8048
- next.push(tool);
8049
- }
8050
- return next;
8051
- }
8052
8233
  function composePluginEvent(consumerHandler, pluginHandlers) {
8053
8234
  const activePluginHandlers = pluginHandlers.filter(
8054
8235
  (handler) => handler != null
@@ -8164,6 +8345,7 @@ var VectorViewport = react.forwardRef(
8164
8345
  toolId = "hand",
8165
8346
  applePencilNav = false,
8166
8347
  interactive = false,
8348
+ readOnlyInteraction,
8167
8349
  selectedIds: selectedIdsProp,
8168
8350
  onSelectionChange,
8169
8351
  onItemsChange: consumerOnItemsChange,
@@ -8175,6 +8357,8 @@ var VectorViewport = react.forwardRef(
8175
8357
  navMenu,
8176
8358
  selectionInspector,
8177
8359
  selectionInspectorProps,
8360
+ contextMenu: renderContextMenu,
8361
+ contextMenuItems: consumerContextMenuItems,
8178
8362
  plugins = [],
8179
8363
  onCameraChange: consumerOnCameraChange,
8180
8364
  customPlacement: consumerCustomPlacement,
@@ -8233,7 +8417,7 @@ var VectorViewport = react.forwardRef(
8233
8417
  let nextTools = [...DEFAULT_VECTOR_TOOLS];
8234
8418
  for (const contribution of orderedPluginContributions) {
8235
8419
  if (contribution.tools?.length) {
8236
- nextTools = mergeToolListById(nextTools, contribution.tools);
8420
+ nextTools = mergeById(nextTools, contribution.tools);
8237
8421
  }
8238
8422
  if (contribution.toolTransform) {
8239
8423
  nextTools = contribution.toolTransform(nextTools);
@@ -8241,6 +8425,16 @@ var VectorViewport = react.forwardRef(
8241
8425
  }
8242
8426
  return nextTools;
8243
8427
  }, [orderedPluginContributions]);
8428
+ const resolvedContextMenuItems = react.useMemo(
8429
+ () => mergeById(
8430
+ DEFAULT_SHAPE_CONTEXT_MENU_ITEMS,
8431
+ ...orderedPluginContributions.map(
8432
+ (contribution) => contribution.contextMenuItems
8433
+ ),
8434
+ consumerContextMenuItems
8435
+ ),
8436
+ [consumerContextMenuItems, orderedPluginContributions]
8437
+ );
8244
8438
  const allCustomPlacements = react.useMemo(() => {
8245
8439
  const placements = [];
8246
8440
  if (consumerCustomPlacement) placements.push(consumerCustomPlacement);
@@ -8366,6 +8560,8 @@ var VectorViewport = react.forwardRef(
8366
8560
  );
8367
8561
  const toolIdRef = react.useRef(toolId);
8368
8562
  const interactiveRef = react.useRef(interactive);
8563
+ const readOnlyInteractionRef = react.useRef(readOnlyInteraction);
8564
+ readOnlyInteractionRef.current = readOnlyInteraction;
8369
8565
  const reducedMotionRef = react.useRef(false);
8370
8566
  const itemsRef = react.useRef(items);
8371
8567
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
@@ -8379,6 +8575,7 @@ var VectorViewport = react.forwardRef(
8379
8575
  const allCustomPlacementsRef = react.useRef(allCustomPlacements);
8380
8576
  allCustomPlacementsRef.current = allCustomPlacements;
8381
8577
  const dragStateRef = react.useRef({ kind: "idle" });
8578
+ const readOnlyItemClickStateRef = react.useRef(null);
8382
8579
  const clipboardRef = react.useRef(null);
8383
8580
  const undoStackRef = react.useRef([]);
8384
8581
  const redoStackRef = react.useRef([]);
@@ -8417,7 +8614,7 @@ var VectorViewport = react.forwardRef(
8417
8614
  isUndoingRef.current = true;
8418
8615
  onItemsChangeRef.current?.(next, { motive: "redo" });
8419
8616
  }, []);
8420
- const [contextMenu, setContextMenu] = react.useState(null);
8617
+ const [contextMenuState, setContextMenuState] = react.useState(null);
8421
8618
  const [uncontrolledSel, setUncontrolledSel] = react.useState([]);
8422
8619
  const isSelectionControlled = onSelectionChange !== void 0;
8423
8620
  const controlledSelectedKey = JSON.stringify(selectedIdsProp ?? []);
@@ -8556,6 +8753,31 @@ var VectorViewport = react.forwardRef(
8556
8753
  );
8557
8754
  const resolvedItemsRef = react.useRef(resolvedItems);
8558
8755
  resolvedItemsRef.current = resolvedItems;
8756
+ const readOnlyActivationResolutionCacheRef = react.useRef(/* @__PURE__ */ new WeakMap());
8757
+ const resolveReadOnlyActivation = react.useCallback(
8758
+ (pointer) => {
8759
+ const cache = readOnlyActivationResolutionCacheRef.current;
8760
+ if (cache.has(pointer)) return cache.get(pointer) ?? null;
8761
+ let target = null;
8762
+ const cam = cameraRef.current;
8763
+ const container = sceneContainerRef.current;
8764
+ if (!interactiveRef.current && toolIdRef.current === "select" && cam && container) {
8765
+ target = resolveReadOnlyActivationTarget({
8766
+ pointer,
8767
+ camera: cam,
8768
+ container,
8769
+ items: resolvedItemsRef.current,
8770
+ detailItems: itemsRef.current,
8771
+ placements: allCustomPlacementsRef.current,
8772
+ scope: readOnlyInteractionRef.current?.itemClicks ?? "custom",
8773
+ selectedIds: effectiveSelectedIdsRef.current
8774
+ });
8775
+ }
8776
+ cache.set(pointer, target);
8777
+ return target;
8778
+ },
8779
+ []
8780
+ );
8559
8781
  const liveId = react.useId();
8560
8782
  const reducedMotion = usePrefersReducedMotion();
8561
8783
  reducedMotionRef.current = reducedMotion;
@@ -8985,12 +9207,14 @@ var VectorViewport = react.forwardRef(
8985
9207
  onUpdate: renderFrame,
8986
9208
  wheelElement: wrapperRef.current ?? void 0,
8987
9209
  touchHandledElsewhere: applePencilNav,
8988
- allowPrimaryPointerPan: () => {
9210
+ allowPrimaryPointerPan: (event) => {
8989
9211
  if (interactiveRef.current) {
8990
9212
  return toolIdRef.current === "hand";
8991
9213
  }
8992
9214
  const t = toolIdRef.current;
8993
- return t === "hand" || t === "select";
9215
+ if (t === "hand") return true;
9216
+ if (t !== "select") return false;
9217
+ return resolveReadOnlyActivation(event) === null;
8994
9218
  }
8995
9219
  });
8996
9220
  let detachPencil;
@@ -9011,7 +9235,7 @@ var VectorViewport = react.forwardRef(
9011
9235
  cameraRef.current = null;
9012
9236
  setCameraForOverlay(null);
9013
9237
  };
9014
- }, [applePencilNav, renderFrame]);
9238
+ }, [applePencilNav, renderFrame, resolveReadOnlyActivation]);
9015
9239
  react.useEffect(() => {
9016
9240
  rendererRef.current?.setInteractionState({
9017
9241
  selectedIds: effectiveSelectedIds,
@@ -9525,6 +9749,29 @@ var VectorViewport = react.forwardRef(
9525
9749
  const rect = el.getBoundingClientRect();
9526
9750
  return cam.screenToWorld(clientX - rect.left, clientY - rect.top);
9527
9751
  }, []);
9752
+ const buildSelectModeItemClickDetail = react.useCallback(
9753
+ (item, world, pointer) => ({
9754
+ item,
9755
+ worldX: world.worldX,
9756
+ worldY: world.worldY,
9757
+ clientX: pointer.clientX,
9758
+ clientY: pointer.clientY,
9759
+ pointerType: pointer.pointerType,
9760
+ shiftKey: pointer.shiftKey,
9761
+ altKey: pointer.altKey,
9762
+ metaKey: pointer.metaKey,
9763
+ ctrlKey: pointer.ctrlKey,
9764
+ items: itemsRef.current,
9765
+ updateItem: (next) => {
9766
+ onItemsChangeRef.current?.(replaceItem(itemsRef.current, item.id, next), {
9767
+ motive: "custom",
9768
+ itemIds: [item.id]
9769
+ });
9770
+ },
9771
+ setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
9772
+ }),
9773
+ []
9774
+ );
9528
9775
  const handleOverlayContextMenu = react.useCallback(
9529
9776
  (e) => {
9530
9777
  if (!interactiveRef.current || !onItemsChangeRef.current) return;
@@ -9539,7 +9786,7 @@ var VectorViewport = react.forwardRef(
9539
9786
  ignoreLocked: true
9540
9787
  });
9541
9788
  if (!hit) {
9542
- setContextMenu(null);
9789
+ setContextMenuState(null);
9543
9790
  return;
9544
9791
  }
9545
9792
  const cur = effectiveSelectedIdsRef.current;
@@ -9550,7 +9797,7 @@ var VectorViewport = react.forwardRef(
9550
9797
  } else {
9551
9798
  nextIds = cur;
9552
9799
  }
9553
- setContextMenu({
9800
+ setContextMenuState({
9554
9801
  x: e.clientX,
9555
9802
  y: e.clientY,
9556
9803
  itemIds: nextIds
@@ -9951,6 +10198,118 @@ var VectorViewport = react.forwardRef(
9951
10198
  },
9952
10199
  [screenToWorld]
9953
10200
  );
10201
+ react.useEffect(() => {
10202
+ const root = interactionRootRef.current;
10203
+ if (!root) return;
10204
+ const onReadOnlyPointerDownCapture = (e) => {
10205
+ if (e.button !== 0) return;
10206
+ if (readOnlyItemClickStateRef.current) return;
10207
+ const target = resolveReadOnlyActivation(e);
10208
+ if (!target) return;
10209
+ const accepted = startCanvuInteraction({
10210
+ kind: "select-mode-item-click",
10211
+ toolId: "select",
10212
+ pointerType: e.pointerType,
10213
+ button: e.button,
10214
+ worldX: target.worldX,
10215
+ worldY: target.worldY,
10216
+ clientX: e.clientX,
10217
+ clientY: e.clientY,
10218
+ shiftKey: e.shiftKey,
10219
+ altKey: e.altKey,
10220
+ metaKey: e.metaKey,
10221
+ ctrlKey: e.ctrlKey,
10222
+ itemIds: [target.item.id]
10223
+ });
10224
+ if (!accepted) {
10225
+ e.preventDefault();
10226
+ e.stopPropagation();
10227
+ return;
10228
+ }
10229
+ wrapperRef.current?.focus({ preventScroll: true });
10230
+ readOnlyItemClickStateRef.current = createReadOnlyActivationSession(
10231
+ target,
10232
+ e
10233
+ );
10234
+ e.preventDefault();
10235
+ e.stopPropagation();
10236
+ };
10237
+ root.addEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10238
+ capture: true
10239
+ });
10240
+ return () => {
10241
+ root.removeEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10242
+ capture: true
10243
+ });
10244
+ };
10245
+ }, [resolveReadOnlyActivation, startCanvuInteraction]);
10246
+ react.useEffect(() => {
10247
+ const finishReadOnlyClick = (ev) => {
10248
+ const st = readOnlyItemClickStateRef.current;
10249
+ if (!st || st.pointerId !== ev.pointerId) return;
10250
+ readOnlyItemClickStateRef.current = null;
10251
+ const world = screenToWorld(ev.clientX, ev.clientY);
10252
+ const current = {
10253
+ worldX: world.worldX,
10254
+ worldY: world.worldY,
10255
+ clientX: ev.clientX,
10256
+ clientY: ev.clientY
10257
+ };
10258
+ updateCanvuInteractionCurrent(current);
10259
+ if (ev.type === "pointercancel") {
10260
+ finishCanvuInteraction("cancelled", { current });
10261
+ return;
10262
+ }
10263
+ if (didReadOnlyActivationMovePastTap(st, ev, TAP_PX)) {
10264
+ finishCanvuInteraction("cancelled", { current });
10265
+ return;
10266
+ }
10267
+ const item = itemsRef.current.find((candidate) => candidate.id === st.itemId) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.itemId);
10268
+ if (!item) {
10269
+ finishCanvuInteraction("cancelled", { current });
10270
+ return;
10271
+ }
10272
+ const detail = buildSelectModeItemClickDetail(item, world, ev);
10273
+ if (st.activation === "custom") {
10274
+ const placement = findReadOnlyItemClickPlacement(
10275
+ item,
10276
+ allCustomPlacementsRef.current
10277
+ );
10278
+ const onSelectModeItemClick = placement?.onSelectModeItemClick;
10279
+ if (!onSelectModeItemClick) {
10280
+ finishCanvuInteraction("cancelled", { current });
10281
+ return;
10282
+ }
10283
+ onSelectModeItemClick(detail);
10284
+ finishCanvuInteraction("completed", {
10285
+ current,
10286
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10287
+ });
10288
+ return;
10289
+ }
10290
+ const handled = readOnlyInteractionRef.current?.onItemClick?.(detail) === "handled";
10291
+ if (!handled) {
10292
+ const cur = effectiveSelectedIdsRef.current;
10293
+ const next = ev.shiftKey ? cur.includes(item.id) ? cur.filter((id) => id !== item.id) : [...cur, item.id] : [item.id];
10294
+ setEffectiveSelectedIdsRef.current(next);
10295
+ }
10296
+ finishCanvuInteraction("completed", {
10297
+ current,
10298
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10299
+ });
10300
+ };
10301
+ document.addEventListener("pointerup", finishReadOnlyClick);
10302
+ document.addEventListener("pointercancel", finishReadOnlyClick);
10303
+ return () => {
10304
+ document.removeEventListener("pointerup", finishReadOnlyClick);
10305
+ document.removeEventListener("pointercancel", finishReadOnlyClick);
10306
+ };
10307
+ }, [
10308
+ buildSelectModeItemClickDetail,
10309
+ finishCanvuInteraction,
10310
+ screenToWorld,
10311
+ updateCanvuInteractionCurrent
10312
+ ]);
9954
10313
  const handleOverlayPointerDown = react.useCallback(
9955
10314
  (e) => {
9956
10315
  let currentDragState = dragStateRef.current;
@@ -9988,7 +10347,7 @@ var VectorViewport = react.forwardRef(
9988
10347
  if (e.button !== 0) return;
9989
10348
  if (editingTextIdRef.current) return;
9990
10349
  wrapperRef.current?.focus({ preventScroll: true });
9991
- setContextMenu(null);
10350
+ setContextMenuState(null);
9992
10351
  const tool = toolIdRef.current;
9993
10352
  if (tool === "hand") return;
9994
10353
  if (applePencilNav && e.pointerType === "pen" && navigator.maxTouchPoints > 0 && (tool === "draw" || tool === "marker")) {
@@ -10129,7 +10488,7 @@ var VectorViewport = react.forwardRef(
10129
10488
  ignoreLocked: true
10130
10489
  });
10131
10490
  if (hit) {
10132
- const selectModeClickPlacement = !e.shiftKey ? findSelectModeItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10491
+ const selectModeClickPlacement = !e.shiftKey ? findReadOnlyItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10133
10492
  if (selectModeClickPlacement) {
10134
10493
  const isAlreadySelected = cur.includes(hit.id);
10135
10494
  const moveIds = isAlreadySelected ? [...cur] : [hit.id];
@@ -10408,7 +10767,7 @@ var VectorViewport = react.forwardRef(
10408
10767
  return;
10409
10768
  }
10410
10769
  wrapperRef.current?.focus({ preventScroll: true });
10411
- setContextMenu(null);
10770
+ setContextMenuState(null);
10412
10771
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10413
10772
  const itemId = createShapeId();
10414
10773
  const item = createFreehandStrokeItem(
@@ -10531,7 +10890,7 @@ var VectorViewport = react.forwardRef(
10531
10890
  return;
10532
10891
  }
10533
10892
  wrapperRef.current?.focus({ preventScroll: true });
10534
- setContextMenu(null);
10893
+ setContextMenuState(null);
10535
10894
  penDetectedRef.current = true;
10536
10895
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10537
10896
  const itemId = createShapeId();
@@ -11036,7 +11395,7 @@ var VectorViewport = react.forwardRef(
11036
11395
  });
11037
11396
  return;
11038
11397
  }
11039
- const placement = findSelectModeItemClickPlacement(
11398
+ const placement = findReadOnlyItemClickPlacement(
11040
11399
  item,
11041
11400
  allCustomPlacementsRef.current
11042
11401
  );
@@ -11047,27 +11406,9 @@ var VectorViewport = react.forwardRef(
11047
11406
  });
11048
11407
  return;
11049
11408
  }
11050
- const { worldX, worldY } = currentWorld;
11051
- onSelectModeItemClick({
11052
- item,
11053
- worldX,
11054
- worldY,
11055
- clientX: ev.clientX,
11056
- clientY: ev.clientY,
11057
- pointerType: ev.pointerType,
11058
- shiftKey: ev.shiftKey,
11059
- altKey: ev.altKey,
11060
- metaKey: ev.metaKey,
11061
- ctrlKey: ev.ctrlKey,
11062
- items: itemsRef.current,
11063
- updateItem: (next) => {
11064
- onItemsChangeRef.current?.(
11065
- replaceItem(itemsRef.current, item.id, next),
11066
- { motive: "custom", itemIds: [item.id] }
11067
- );
11068
- },
11069
- setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
11070
- });
11409
+ onSelectModeItemClick(
11410
+ buildSelectModeItemClickDetail(item, currentWorld, ev)
11411
+ );
11071
11412
  finishCanvuInteraction("completed", {
11072
11413
  current: currentInteractionPoint,
11073
11414
  info: { motive: "custom", itemIds: [item.id], toolId: "select" }
@@ -11437,6 +11778,7 @@ var VectorViewport = react.forwardRef(
11437
11778
  document.removeEventListener("pointercancel", onUp);
11438
11779
  };
11439
11780
  }, [
11781
+ buildSelectModeItemClickDetail,
11440
11782
  emitRemoteStrokePreview,
11441
11783
  emitRemoteStrokePreviewClear,
11442
11784
  interactive,
@@ -11588,6 +11930,66 @@ var VectorViewport = react.forwardRef(
11588
11930
  const defaultNavMenu = cameraForOverlay ? /* @__PURE__ */ jsxRuntime.jsx(NavMenu, {}) : null;
11589
11931
  const resolvedSelectionInspector = selectionInspector === void 0 ? defaultSelectionInspector : selectionInspector;
11590
11932
  const resolvedNavMenu = navMenu === void 0 ? defaultNavMenu : navMenu;
11933
+ const contextMenuItemIds = contextMenuState?.itemIds ?? [];
11934
+ const contextMenuSelectedItems = contextMenuState ? contextMenuItemIds.map((id) => items.find((item) => item.id === id)).filter((item) => item != null) : [];
11935
+ const allContextMenuSelectedLocked = contextMenuItemIds.length > 0 && contextMenuItemIds.every((id) => items.find((item) => item.id === id)?.locked);
11936
+ const closeContextMenu = () => setContextMenuState(null);
11937
+ const runContextMenuAction = (fn) => () => {
11938
+ fn();
11939
+ closeContextMenu();
11940
+ };
11941
+ const contextMenuRenderContext = contextMenuState ? {
11942
+ position: { x: contextMenuState.x, y: contextMenuState.y },
11943
+ selectedIds: contextMenuItemIds,
11944
+ selectedItems: contextMenuSelectedItems,
11945
+ allSelectedLocked: allContextMenuSelectedLocked,
11946
+ anySelectedLocked: contextMenuSelectedItems.some((item) => item.locked),
11947
+ close: closeContextMenu,
11948
+ cut: runContextMenuAction(() => cutIds(contextMenuItemIds)),
11949
+ copy: runContextMenuAction(
11950
+ () => copyIdsToInternalClipboard(contextMenuItemIds)
11951
+ ),
11952
+ duplicate: runContextMenuAction(() => duplicateIds(contextMenuItemIds)),
11953
+ toggleLock: runContextMenuAction(
11954
+ () => setLockedOnIds(contextMenuItemIds, !allContextMenuSelectedLocked)
11955
+ ),
11956
+ bringToFront: runContextMenuAction(
11957
+ () => reorderIds(contextMenuItemIds, "front")
11958
+ ),
11959
+ bringForward: runContextMenuAction(
11960
+ () => reorderIds(contextMenuItemIds, "forward")
11961
+ ),
11962
+ sendBackward: runContextMenuAction(
11963
+ () => reorderIds(contextMenuItemIds, "backward")
11964
+ ),
11965
+ sendToBack: runContextMenuAction(
11966
+ () => reorderIds(contextMenuItemIds, "back")
11967
+ ),
11968
+ delete: runContextMenuAction(() => deleteIds(contextMenuItemIds)),
11969
+ deleteSelection: runContextMenuAction(() => deleteIds(contextMenuItemIds)),
11970
+ items: resolvedContextMenuItems
11971
+ } : null;
11972
+ const resolvedContextMenu = interactive && onItemsChange && contextMenuState && contextMenuRenderContext && renderContextMenu !== null ? renderContextMenu ? renderContextMenu(contextMenuRenderContext) : /* @__PURE__ */ jsxRuntime.jsx(
11973
+ ShapeContextMenu,
11974
+ {
11975
+ x: contextMenuState.x,
11976
+ y: contextMenuState.y,
11977
+ selectedIds: contextMenuItemIds,
11978
+ selectedItems: contextMenuSelectedItems,
11979
+ allSelectedLocked: allContextMenuSelectedLocked,
11980
+ items: resolvedContextMenuItems,
11981
+ onClose: closeContextMenu,
11982
+ onToggleLock: () => setLockedOnIds(contextMenuItemIds, !allContextMenuSelectedLocked),
11983
+ onCut: () => cutIds(contextMenuItemIds),
11984
+ onCopy: () => copyIdsToInternalClipboard(contextMenuItemIds),
11985
+ onBringToFront: () => reorderIds(contextMenuItemIds, "front"),
11986
+ onBringForward: () => reorderIds(contextMenuItemIds, "forward"),
11987
+ onSendBackward: () => reorderIds(contextMenuItemIds, "backward"),
11988
+ onSendToBack: () => reorderIds(contextMenuItemIds, "back"),
11989
+ onDuplicate: () => duplicateIds(contextMenuItemIds),
11990
+ onDelete: () => deleteIds(contextMenuItemIds)
11991
+ }
11992
+ ) : null;
11591
11993
  return /* @__PURE__ */ jsxRuntime.jsx(CanvuPluginContext.Provider, { value: pluginContextValue, children: /* @__PURE__ */ jsxRuntime.jsx(CanvuChromeContext.Provider, { value: chromeContextValue, children: /* @__PURE__ */ jsxRuntime.jsxs(
11592
11994
  "div",
11593
11995
  {
@@ -11741,30 +12143,7 @@ var VectorViewport = react.forwardRef(
11741
12143
  }
11742
12144
  ),
11743
12145
  resolvedSelectionInspector,
11744
- interactive && onItemsChange && contextMenu && /* @__PURE__ */ jsxRuntime.jsx(
11745
- ShapeContextMenu,
11746
- {
11747
- x: contextMenu.x,
11748
- y: contextMenu.y,
11749
- allSelectedLocked: contextMenu.itemIds.length > 0 && contextMenu.itemIds.every(
11750
- (id) => items.find((i) => i.id === id)?.locked
11751
- ),
11752
- onClose: () => setContextMenu(null),
11753
- onToggleLock: () => {
11754
- const ids = contextMenu.itemIds;
11755
- const allLocked = ids.length > 0 && ids.every((id) => items.find((i) => i.id === id)?.locked);
11756
- setLockedOnIds(ids, !allLocked);
11757
- },
11758
- onCut: () => cutIds(contextMenu.itemIds),
11759
- onCopy: () => copyIdsToInternalClipboard(contextMenu.itemIds),
11760
- onBringToFront: () => reorderIds(contextMenu.itemIds, "front"),
11761
- onBringForward: () => reorderIds(contextMenu.itemIds, "forward"),
11762
- onSendBackward: () => reorderIds(contextMenu.itemIds, "backward"),
11763
- onSendToBack: () => reorderIds(contextMenu.itemIds, "back"),
11764
- onDuplicate: () => duplicateIds(contextMenu.itemIds),
11765
- onDelete: () => deleteIds(contextMenu.itemIds)
11766
- }
11767
- ),
12146
+ resolvedContextMenu,
11768
12147
  resolvedNavMenu,
11769
12148
  /* @__PURE__ */ jsxRuntime.jsx(
11770
12149
  "div",
@@ -11801,6 +12180,7 @@ exports.IconSelect = IconSelect;
11801
12180
  exports.IconText = IconText;
11802
12181
  exports.ImagesMenu = ImagesMenu;
11803
12182
  exports.NavMenu = NavMenu;
12183
+ exports.SHAPE_CONTEXT_MENU_ITEM_IDS = SHAPE_CONTEXT_MENU_ITEM_IDS;
11804
12184
  exports.ShapeContextMenu = ShapeContextMenu;
11805
12185
  exports.VectorCanvas = VectorCanvas;
11806
12186
  exports.VectorCanvasBody = VectorCanvasBody;