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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import getStroke from 'perfect-freehand';
2
- import { createContext, forwardRef, useRef, useState, useCallback, useMemo, useId, useEffect, useImperativeHandle, useContext, useLayoutEffect, Children, isValidElement } from 'react';
2
+ import { createContext, forwardRef, useRef, useState, useCallback, useMemo, useId, useEffect, useImperativeHandle, useContext, useLayoutEffect, Fragment as Fragment$1, Children, isValidElement } from 'react';
3
3
  import { useSensors, useSensor, PointerSensor, DndContext } from '@dnd-kit/core';
4
4
  import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
5
5
  import { CSS } from '@dnd-kit/utilities';
@@ -3216,6 +3216,30 @@ function createToolPlugin(options) {
3216
3216
  }
3217
3217
  });
3218
3218
  }
3219
+
3220
+ // src/react/merge-by-id.ts
3221
+ function mergeById(baseItems, ...contributions) {
3222
+ const next = [...baseItems];
3223
+ const indexById = /* @__PURE__ */ new Map();
3224
+ for (const [index, item] of next.entries()) {
3225
+ if (!indexById.has(item.id)) {
3226
+ indexById.set(item.id, index);
3227
+ }
3228
+ }
3229
+ for (const contribution of contributions) {
3230
+ if (!contribution?.length) continue;
3231
+ for (const item of contribution) {
3232
+ const existingIndex = indexById.get(item.id);
3233
+ if (existingIndex !== void 0) {
3234
+ next[existingIndex] = item;
3235
+ continue;
3236
+ }
3237
+ indexById.set(item.id, next.length);
3238
+ next.push(item);
3239
+ }
3240
+ }
3241
+ return next;
3242
+ }
3219
3243
  var menuStyle = {
3220
3244
  position: "fixed",
3221
3245
  zIndex: 1e4,
@@ -3245,9 +3269,97 @@ var dividerStyle = {
3245
3269
  margin: "4px 8px",
3246
3270
  background: "#e2e8f0"
3247
3271
  };
3272
+ var SHAPE_CONTEXT_MENU_ITEM_IDS = {
3273
+ cut: "cut",
3274
+ copy: "copy",
3275
+ duplicate: "duplicate",
3276
+ reorderDivider: "reorder-divider",
3277
+ bringToFront: "bring-to-front",
3278
+ bringForward: "bring-forward",
3279
+ sendBackward: "send-backward",
3280
+ sendToBack: "send-to-back",
3281
+ lockDivider: "lock-divider",
3282
+ toggleLock: "toggle-lock",
3283
+ delete: "delete"
3284
+ };
3285
+ function renderAction(label, onClick, options) {
3286
+ return /* @__PURE__ */ jsx(
3287
+ "button",
3288
+ {
3289
+ type: "button",
3290
+ role: "menuitem",
3291
+ style: {
3292
+ ...itemStyle,
3293
+ ...options?.danger ? { color: "#b91c1c" } : {}
3294
+ },
3295
+ onMouseEnter: (e) => {
3296
+ e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
3297
+ },
3298
+ onMouseLeave: (e) => {
3299
+ e.currentTarget.style.background = "transparent";
3300
+ },
3301
+ onClick,
3302
+ children: label
3303
+ }
3304
+ );
3305
+ }
3306
+ function renderDivider() {
3307
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle });
3308
+ }
3309
+ var DEFAULT_SHAPE_CONTEXT_MENU_ITEMS = [
3310
+ {
3311
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.cut,
3312
+ render: (ctx) => renderAction("Recortar", ctx.cut)
3313
+ },
3314
+ {
3315
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.copy,
3316
+ render: (ctx) => renderAction("Copiar", ctx.copy)
3317
+ },
3318
+ {
3319
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.duplicate,
3320
+ render: (ctx) => renderAction("Duplicar", ctx.duplicate)
3321
+ },
3322
+ {
3323
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.reorderDivider,
3324
+ render: renderDivider
3325
+ },
3326
+ {
3327
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.bringToFront,
3328
+ render: (ctx) => renderAction("Trazer para frente", ctx.bringToFront)
3329
+ },
3330
+ {
3331
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.bringForward,
3332
+ render: (ctx) => renderAction("Avancar uma camada", ctx.bringForward)
3333
+ },
3334
+ {
3335
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.sendBackward,
3336
+ render: (ctx) => renderAction("Recuar uma camada", ctx.sendBackward)
3337
+ },
3338
+ {
3339
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.sendToBack,
3340
+ render: (ctx) => renderAction("Enviar para tras", ctx.sendToBack)
3341
+ },
3342
+ {
3343
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.lockDivider,
3344
+ render: renderDivider
3345
+ },
3346
+ {
3347
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.toggleLock,
3348
+ render: (ctx) => renderAction(
3349
+ ctx.allSelectedLocked ? "Desbloquear" : "Bloquear",
3350
+ ctx.toggleLock
3351
+ )
3352
+ },
3353
+ {
3354
+ id: SHAPE_CONTEXT_MENU_ITEM_IDS.delete,
3355
+ render: (ctx) => renderAction("Apagar", ctx.delete, { danger: true })
3356
+ }
3357
+ ];
3248
3358
  function ShapeContextMenu({
3249
3359
  x,
3250
3360
  y,
3361
+ selectedIds,
3362
+ selectedItems = [],
3251
3363
  allSelectedLocked,
3252
3364
  onClose,
3253
3365
  onToggleLock,
@@ -3258,7 +3370,10 @@ function ShapeContextMenu({
3258
3370
  onSendBackward,
3259
3371
  onSendToBack,
3260
3372
  onDuplicate,
3261
- onDelete
3373
+ onDelete,
3374
+ items,
3375
+ itemContributions,
3376
+ children
3262
3377
  }) {
3263
3378
  const rootRef = useRef(null);
3264
3379
  useLayoutEffect(() => {
@@ -3298,49 +3413,43 @@ function ShapeContextMenu({
3298
3413
  document.removeEventListener("pointerdown", onPointerDown, true);
3299
3414
  };
3300
3415
  }, [onClose]);
3416
+ const menuItems = useMemo(
3417
+ () => mergeById(items ?? DEFAULT_SHAPE_CONTEXT_MENU_ITEMS, itemContributions),
3418
+ [itemContributions, items]
3419
+ );
3301
3420
  const run = (fn) => () => {
3302
3421
  fn();
3303
3422
  onClose();
3304
3423
  };
3305
- const renderAction = (label, onClick, options) => /* @__PURE__ */ jsx(
3306
- "button",
3307
- {
3308
- type: "button",
3309
- role: "menuitem",
3310
- style: {
3311
- ...itemStyle,
3312
- ...options?.danger ? { color: "#b91c1c" } : {}
3313
- },
3314
- onMouseEnter: (e) => {
3315
- e.currentTarget.style.background = options?.danger ? "#fef2f2" : "#f1f5f9";
3316
- },
3317
- onMouseLeave: (e) => {
3318
- e.currentTarget.style.background = "transparent";
3319
- },
3320
- onClick: run(onClick),
3321
- children: label
3322
- }
3323
- );
3324
- const menu = /* @__PURE__ */ jsxs(
3424
+ const resolvedSelectedIds = selectedIds ?? selectedItems.map((item) => item.id);
3425
+ const resolvedAllSelectedLocked = allSelectedLocked ?? (selectedItems.length > 0 && selectedItems.every((item) => item.locked));
3426
+ const menuContext = {
3427
+ position: { x, y },
3428
+ selectedIds: resolvedSelectedIds,
3429
+ selectedItems,
3430
+ allSelectedLocked: resolvedAllSelectedLocked,
3431
+ anySelectedLocked: selectedItems.some((item) => item.locked),
3432
+ close: onClose,
3433
+ cut: run(onCut),
3434
+ copy: run(onCopy),
3435
+ duplicate: run(onDuplicate),
3436
+ toggleLock: run(onToggleLock),
3437
+ bringToFront: run(onBringToFront),
3438
+ bringForward: run(onBringForward),
3439
+ sendBackward: run(onSendBackward),
3440
+ sendToBack: run(onSendToBack),
3441
+ delete: run(onDelete),
3442
+ deleteSelection: run(onDelete)
3443
+ };
3444
+ const content = typeof children === "function" ? children(menuContext) : children !== void 0 ? children : menuItems.map((item) => /* @__PURE__ */ jsx(Fragment$1, { children: item.render(menuContext) }, item.id));
3445
+ const menu = /* @__PURE__ */ jsx(
3325
3446
  "div",
3326
3447
  {
3327
3448
  ref: rootRef,
3328
3449
  "data-slot": "shape-context-menu",
3329
3450
  style: { ...menuStyle, left: x, top: y },
3330
3451
  role: "menu",
3331
- children: [
3332
- renderAction("Recortar", onCut),
3333
- renderAction("Copiar", onCopy),
3334
- renderAction("Duplicar", onDuplicate),
3335
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
3336
- renderAction("Trazer para frente", onBringToFront),
3337
- renderAction("Avancar uma camada", onBringForward),
3338
- renderAction("Recuar uma camada", onSendBackward),
3339
- renderAction("Enviar para tras", onSendToBack),
3340
- /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: dividerStyle }),
3341
- renderAction(allSelectedLocked ? "Desbloquear" : "Bloquear", onToggleLock),
3342
- renderAction("Apagar", onDelete, { danger: true })
3343
- ]
3452
+ children: content
3344
3453
  }
3345
3454
  );
3346
3455
  if (typeof document === "undefined") {
@@ -5055,6 +5164,7 @@ function VectorToolbarComponent({
5055
5164
  value,
5056
5165
  onChange,
5057
5166
  tools,
5167
+ toolContributions,
5058
5168
  overflowToolIds = DEFAULT_OVERFLOW_TOOL_IDS,
5059
5169
  overflowMenuAriaLabel = "More tools",
5060
5170
  "aria-label": ariaLabel = "Canvas tools",
@@ -5072,7 +5182,10 @@ function VectorToolbarComponent({
5072
5182
  }) {
5073
5183
  const pluginContext = useContext(CanvuPluginContext);
5074
5184
  const runtimeTools = pluginContext?.resolvedTools;
5075
- const resolvedTools = tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS;
5185
+ const resolvedTools = useMemo(
5186
+ () => mergeById(tools ?? runtimeTools ?? DEFAULT_VECTOR_TOOLS, toolContributions),
5187
+ [runtimeTools, toolContributions, tools]
5188
+ );
5076
5189
  const { primary: primaryTools, overflow: overflowTools } = splitToolbarTools(
5077
5190
  resolvedTools,
5078
5191
  overflowToolIds
@@ -5646,7 +5759,7 @@ function attachViewportInput(options) {
5646
5759
  if (touchMomentum) {
5647
5760
  touchMomentum.cancel();
5648
5761
  }
5649
- const panOk = allowPrimaryPointerPan();
5762
+ const panOk = allowPrimaryPointerPan(e);
5650
5763
  if (e.pointerType === "mouse" && e.button === 0) {
5651
5764
  if (!panOk) {
5652
5765
  return;
@@ -7670,6 +7783,93 @@ function PresenceRemoteLayer({
7670
7783
  );
7671
7784
  }
7672
7785
 
7786
+ // src/react/read-only-activation.ts
7787
+ function findReadOnlyItemClickPlacement(item, placements) {
7788
+ const toolId = item.customToolId;
7789
+ if (!toolId) return null;
7790
+ return [...placements].reverse().find(
7791
+ (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
7792
+ ) ?? null;
7793
+ }
7794
+ function resolveReadOnlyActivationTarget(input) {
7795
+ const {
7796
+ pointer,
7797
+ camera,
7798
+ container,
7799
+ items,
7800
+ detailItems = items,
7801
+ placements,
7802
+ scope,
7803
+ selectedIds
7804
+ } = input;
7805
+ const rect = container.getBoundingClientRect();
7806
+ const world = camera.screenToWorld(
7807
+ pointer.clientX - rect.left,
7808
+ pointer.clientY - rect.top
7809
+ );
7810
+ const hit = hitTestWorldPoint(items, world.worldX, world.worldY, {
7811
+ lineHitWorld: 10 / camera.zoom,
7812
+ ignoreLocked: true
7813
+ });
7814
+ if (!hit) return null;
7815
+ const customPlacement = findReadOnlyItemClickPlacement(hit, placements);
7816
+ if (customPlacement?.onSelectModeItemClick) {
7817
+ return {
7818
+ item: hit,
7819
+ activation: "custom",
7820
+ worldX: world.worldX,
7821
+ worldY: world.worldY
7822
+ };
7823
+ }
7824
+ if (scope === "all") {
7825
+ return {
7826
+ item: hit,
7827
+ activation: "read-only",
7828
+ worldX: world.worldX,
7829
+ worldY: world.worldY
7830
+ };
7831
+ }
7832
+ if (typeof scope === "function") {
7833
+ const allowed = scope({
7834
+ item: hit,
7835
+ worldX: world.worldX,
7836
+ worldY: world.worldY,
7837
+ clientX: pointer.clientX,
7838
+ clientY: pointer.clientY,
7839
+ pointerType: pointer.pointerType,
7840
+ shiftKey: pointer.shiftKey,
7841
+ altKey: pointer.altKey,
7842
+ metaKey: pointer.metaKey,
7843
+ ctrlKey: pointer.ctrlKey,
7844
+ items: detailItems,
7845
+ selectedIds
7846
+ });
7847
+ if (allowed) {
7848
+ return {
7849
+ item: hit,
7850
+ activation: "read-only",
7851
+ worldX: world.worldX,
7852
+ worldY: world.worldY
7853
+ };
7854
+ }
7855
+ }
7856
+ return null;
7857
+ }
7858
+ function createReadOnlyActivationSession(target, pointer) {
7859
+ return {
7860
+ pointerId: pointer.pointerId,
7861
+ itemId: target.item.id,
7862
+ activation: target.activation,
7863
+ startWorld: { x: target.worldX, y: target.worldY },
7864
+ startScreen: { x: pointer.clientX, y: pointer.clientY }
7865
+ };
7866
+ }
7867
+ function didReadOnlyActivationMovePastTap(session, pointer, tapPx) {
7868
+ const dx = pointer.clientX - session.startScreen.x;
7869
+ const dy = pointer.clientY - session.startScreen.y;
7870
+ return Math.hypot(dx, dy) > tapPx;
7871
+ }
7872
+
7673
7873
  // src/react/stable-selection.ts
7674
7874
  function shallowEqualStringArray(a, b) {
7675
7875
  if (a === b) return true;
@@ -8023,25 +8223,6 @@ function isDefaultMarkerToolStyle(style) {
8023
8223
  function tagCustomPlacementItem(item, toolId) {
8024
8224
  return item.customToolId === toolId ? item : { ...item, customToolId: toolId };
8025
8225
  }
8026
- function findSelectModeItemClickPlacement(item, placements) {
8027
- const toolId = item.customToolId;
8028
- if (!toolId) return null;
8029
- return [...placements].reverse().find(
8030
- (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
8031
- ) ?? null;
8032
- }
8033
- function mergeToolListById(baseTools, pluginTools) {
8034
- const next = [...baseTools];
8035
- for (const tool of pluginTools) {
8036
- const index = next.findIndex((candidate) => candidate.id === tool.id);
8037
- if (index >= 0) {
8038
- next[index] = tool;
8039
- continue;
8040
- }
8041
- next.push(tool);
8042
- }
8043
- return next;
8044
- }
8045
8226
  function composePluginEvent(consumerHandler, pluginHandlers) {
8046
8227
  const activePluginHandlers = pluginHandlers.filter(
8047
8228
  (handler) => handler != null
@@ -8157,6 +8338,7 @@ var VectorViewport = forwardRef(
8157
8338
  toolId = "hand",
8158
8339
  applePencilNav = false,
8159
8340
  interactive = false,
8341
+ readOnlyInteraction,
8160
8342
  selectedIds: selectedIdsProp,
8161
8343
  onSelectionChange,
8162
8344
  onItemsChange: consumerOnItemsChange,
@@ -8168,6 +8350,8 @@ var VectorViewport = forwardRef(
8168
8350
  navMenu,
8169
8351
  selectionInspector,
8170
8352
  selectionInspectorProps,
8353
+ contextMenu: renderContextMenu,
8354
+ contextMenuItems: consumerContextMenuItems,
8171
8355
  plugins = [],
8172
8356
  onCameraChange: consumerOnCameraChange,
8173
8357
  customPlacement: consumerCustomPlacement,
@@ -8226,7 +8410,7 @@ var VectorViewport = forwardRef(
8226
8410
  let nextTools = [...DEFAULT_VECTOR_TOOLS];
8227
8411
  for (const contribution of orderedPluginContributions) {
8228
8412
  if (contribution.tools?.length) {
8229
- nextTools = mergeToolListById(nextTools, contribution.tools);
8413
+ nextTools = mergeById(nextTools, contribution.tools);
8230
8414
  }
8231
8415
  if (contribution.toolTransform) {
8232
8416
  nextTools = contribution.toolTransform(nextTools);
@@ -8234,6 +8418,16 @@ var VectorViewport = forwardRef(
8234
8418
  }
8235
8419
  return nextTools;
8236
8420
  }, [orderedPluginContributions]);
8421
+ const resolvedContextMenuItems = useMemo(
8422
+ () => mergeById(
8423
+ DEFAULT_SHAPE_CONTEXT_MENU_ITEMS,
8424
+ ...orderedPluginContributions.map(
8425
+ (contribution) => contribution.contextMenuItems
8426
+ ),
8427
+ consumerContextMenuItems
8428
+ ),
8429
+ [consumerContextMenuItems, orderedPluginContributions]
8430
+ );
8237
8431
  const allCustomPlacements = useMemo(() => {
8238
8432
  const placements = [];
8239
8433
  if (consumerCustomPlacement) placements.push(consumerCustomPlacement);
@@ -8359,6 +8553,8 @@ var VectorViewport = forwardRef(
8359
8553
  );
8360
8554
  const toolIdRef = useRef(toolId);
8361
8555
  const interactiveRef = useRef(interactive);
8556
+ const readOnlyInteractionRef = useRef(readOnlyInteraction);
8557
+ readOnlyInteractionRef.current = readOnlyInteraction;
8362
8558
  const reducedMotionRef = useRef(false);
8363
8559
  const itemsRef = useRef(items);
8364
8560
  const onWorldPointerDownRef = useRef(onWorldPointerDown);
@@ -8372,6 +8568,7 @@ var VectorViewport = forwardRef(
8372
8568
  const allCustomPlacementsRef = useRef(allCustomPlacements);
8373
8569
  allCustomPlacementsRef.current = allCustomPlacements;
8374
8570
  const dragStateRef = useRef({ kind: "idle" });
8571
+ const readOnlyItemClickStateRef = useRef(null);
8375
8572
  const clipboardRef = useRef(null);
8376
8573
  const undoStackRef = useRef([]);
8377
8574
  const redoStackRef = useRef([]);
@@ -8410,7 +8607,7 @@ var VectorViewport = forwardRef(
8410
8607
  isUndoingRef.current = true;
8411
8608
  onItemsChangeRef.current?.(next, { motive: "redo" });
8412
8609
  }, []);
8413
- const [contextMenu, setContextMenu] = useState(null);
8610
+ const [contextMenuState, setContextMenuState] = useState(null);
8414
8611
  const [uncontrolledSel, setUncontrolledSel] = useState([]);
8415
8612
  const isSelectionControlled = onSelectionChange !== void 0;
8416
8613
  const controlledSelectedKey = JSON.stringify(selectedIdsProp ?? []);
@@ -8549,6 +8746,31 @@ var VectorViewport = forwardRef(
8549
8746
  );
8550
8747
  const resolvedItemsRef = useRef(resolvedItems);
8551
8748
  resolvedItemsRef.current = resolvedItems;
8749
+ const readOnlyActivationResolutionCacheRef = useRef(/* @__PURE__ */ new WeakMap());
8750
+ const resolveReadOnlyActivation = useCallback(
8751
+ (pointer) => {
8752
+ const cache = readOnlyActivationResolutionCacheRef.current;
8753
+ if (cache.has(pointer)) return cache.get(pointer) ?? null;
8754
+ let target = null;
8755
+ const cam = cameraRef.current;
8756
+ const container = sceneContainerRef.current;
8757
+ if (!interactiveRef.current && toolIdRef.current === "select" && cam && container) {
8758
+ target = resolveReadOnlyActivationTarget({
8759
+ pointer,
8760
+ camera: cam,
8761
+ container,
8762
+ items: resolvedItemsRef.current,
8763
+ detailItems: itemsRef.current,
8764
+ placements: allCustomPlacementsRef.current,
8765
+ scope: readOnlyInteractionRef.current?.itemClicks ?? "custom",
8766
+ selectedIds: effectiveSelectedIdsRef.current
8767
+ });
8768
+ }
8769
+ cache.set(pointer, target);
8770
+ return target;
8771
+ },
8772
+ []
8773
+ );
8552
8774
  const liveId = useId();
8553
8775
  const reducedMotion = usePrefersReducedMotion();
8554
8776
  reducedMotionRef.current = reducedMotion;
@@ -8978,12 +9200,14 @@ var VectorViewport = forwardRef(
8978
9200
  onUpdate: renderFrame,
8979
9201
  wheelElement: wrapperRef.current ?? void 0,
8980
9202
  touchHandledElsewhere: applePencilNav,
8981
- allowPrimaryPointerPan: () => {
9203
+ allowPrimaryPointerPan: (event) => {
8982
9204
  if (interactiveRef.current) {
8983
9205
  return toolIdRef.current === "hand";
8984
9206
  }
8985
9207
  const t = toolIdRef.current;
8986
- return t === "hand" || t === "select";
9208
+ if (t === "hand") return true;
9209
+ if (t !== "select") return false;
9210
+ return resolveReadOnlyActivation(event) === null;
8987
9211
  }
8988
9212
  });
8989
9213
  let detachPencil;
@@ -9004,7 +9228,7 @@ var VectorViewport = forwardRef(
9004
9228
  cameraRef.current = null;
9005
9229
  setCameraForOverlay(null);
9006
9230
  };
9007
- }, [applePencilNav, renderFrame]);
9231
+ }, [applePencilNav, renderFrame, resolveReadOnlyActivation]);
9008
9232
  useEffect(() => {
9009
9233
  rendererRef.current?.setInteractionState({
9010
9234
  selectedIds: effectiveSelectedIds,
@@ -9518,6 +9742,29 @@ var VectorViewport = forwardRef(
9518
9742
  const rect = el.getBoundingClientRect();
9519
9743
  return cam.screenToWorld(clientX - rect.left, clientY - rect.top);
9520
9744
  }, []);
9745
+ const buildSelectModeItemClickDetail = useCallback(
9746
+ (item, world, pointer) => ({
9747
+ item,
9748
+ worldX: world.worldX,
9749
+ worldY: world.worldY,
9750
+ clientX: pointer.clientX,
9751
+ clientY: pointer.clientY,
9752
+ pointerType: pointer.pointerType,
9753
+ shiftKey: pointer.shiftKey,
9754
+ altKey: pointer.altKey,
9755
+ metaKey: pointer.metaKey,
9756
+ ctrlKey: pointer.ctrlKey,
9757
+ items: itemsRef.current,
9758
+ updateItem: (next) => {
9759
+ onItemsChangeRef.current?.(replaceItem(itemsRef.current, item.id, next), {
9760
+ motive: "custom",
9761
+ itemIds: [item.id]
9762
+ });
9763
+ },
9764
+ setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
9765
+ }),
9766
+ []
9767
+ );
9521
9768
  const handleOverlayContextMenu = useCallback(
9522
9769
  (e) => {
9523
9770
  if (!interactiveRef.current || !onItemsChangeRef.current) return;
@@ -9532,7 +9779,7 @@ var VectorViewport = forwardRef(
9532
9779
  ignoreLocked: true
9533
9780
  });
9534
9781
  if (!hit) {
9535
- setContextMenu(null);
9782
+ setContextMenuState(null);
9536
9783
  return;
9537
9784
  }
9538
9785
  const cur = effectiveSelectedIdsRef.current;
@@ -9543,7 +9790,7 @@ var VectorViewport = forwardRef(
9543
9790
  } else {
9544
9791
  nextIds = cur;
9545
9792
  }
9546
- setContextMenu({
9793
+ setContextMenuState({
9547
9794
  x: e.clientX,
9548
9795
  y: e.clientY,
9549
9796
  itemIds: nextIds
@@ -9944,6 +10191,118 @@ var VectorViewport = forwardRef(
9944
10191
  },
9945
10192
  [screenToWorld]
9946
10193
  );
10194
+ useEffect(() => {
10195
+ const root = interactionRootRef.current;
10196
+ if (!root) return;
10197
+ const onReadOnlyPointerDownCapture = (e) => {
10198
+ if (e.button !== 0) return;
10199
+ if (readOnlyItemClickStateRef.current) return;
10200
+ const target = resolveReadOnlyActivation(e);
10201
+ if (!target) return;
10202
+ const accepted = startCanvuInteraction({
10203
+ kind: "select-mode-item-click",
10204
+ toolId: "select",
10205
+ pointerType: e.pointerType,
10206
+ button: e.button,
10207
+ worldX: target.worldX,
10208
+ worldY: target.worldY,
10209
+ clientX: e.clientX,
10210
+ clientY: e.clientY,
10211
+ shiftKey: e.shiftKey,
10212
+ altKey: e.altKey,
10213
+ metaKey: e.metaKey,
10214
+ ctrlKey: e.ctrlKey,
10215
+ itemIds: [target.item.id]
10216
+ });
10217
+ if (!accepted) {
10218
+ e.preventDefault();
10219
+ e.stopPropagation();
10220
+ return;
10221
+ }
10222
+ wrapperRef.current?.focus({ preventScroll: true });
10223
+ readOnlyItemClickStateRef.current = createReadOnlyActivationSession(
10224
+ target,
10225
+ e
10226
+ );
10227
+ e.preventDefault();
10228
+ e.stopPropagation();
10229
+ };
10230
+ root.addEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10231
+ capture: true
10232
+ });
10233
+ return () => {
10234
+ root.removeEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10235
+ capture: true
10236
+ });
10237
+ };
10238
+ }, [resolveReadOnlyActivation, startCanvuInteraction]);
10239
+ useEffect(() => {
10240
+ const finishReadOnlyClick = (ev) => {
10241
+ const st = readOnlyItemClickStateRef.current;
10242
+ if (!st || st.pointerId !== ev.pointerId) return;
10243
+ readOnlyItemClickStateRef.current = null;
10244
+ const world = screenToWorld(ev.clientX, ev.clientY);
10245
+ const current = {
10246
+ worldX: world.worldX,
10247
+ worldY: world.worldY,
10248
+ clientX: ev.clientX,
10249
+ clientY: ev.clientY
10250
+ };
10251
+ updateCanvuInteractionCurrent(current);
10252
+ if (ev.type === "pointercancel") {
10253
+ finishCanvuInteraction("cancelled", { current });
10254
+ return;
10255
+ }
10256
+ if (didReadOnlyActivationMovePastTap(st, ev, TAP_PX)) {
10257
+ finishCanvuInteraction("cancelled", { current });
10258
+ return;
10259
+ }
10260
+ const item = itemsRef.current.find((candidate) => candidate.id === st.itemId) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.itemId);
10261
+ if (!item) {
10262
+ finishCanvuInteraction("cancelled", { current });
10263
+ return;
10264
+ }
10265
+ const detail = buildSelectModeItemClickDetail(item, world, ev);
10266
+ if (st.activation === "custom") {
10267
+ const placement = findReadOnlyItemClickPlacement(
10268
+ item,
10269
+ allCustomPlacementsRef.current
10270
+ );
10271
+ const onSelectModeItemClick = placement?.onSelectModeItemClick;
10272
+ if (!onSelectModeItemClick) {
10273
+ finishCanvuInteraction("cancelled", { current });
10274
+ return;
10275
+ }
10276
+ onSelectModeItemClick(detail);
10277
+ finishCanvuInteraction("completed", {
10278
+ current,
10279
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10280
+ });
10281
+ return;
10282
+ }
10283
+ const handled = readOnlyInteractionRef.current?.onItemClick?.(detail) === "handled";
10284
+ if (!handled) {
10285
+ const cur = effectiveSelectedIdsRef.current;
10286
+ const next = ev.shiftKey ? cur.includes(item.id) ? cur.filter((id) => id !== item.id) : [...cur, item.id] : [item.id];
10287
+ setEffectiveSelectedIdsRef.current(next);
10288
+ }
10289
+ finishCanvuInteraction("completed", {
10290
+ current,
10291
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10292
+ });
10293
+ };
10294
+ document.addEventListener("pointerup", finishReadOnlyClick);
10295
+ document.addEventListener("pointercancel", finishReadOnlyClick);
10296
+ return () => {
10297
+ document.removeEventListener("pointerup", finishReadOnlyClick);
10298
+ document.removeEventListener("pointercancel", finishReadOnlyClick);
10299
+ };
10300
+ }, [
10301
+ buildSelectModeItemClickDetail,
10302
+ finishCanvuInteraction,
10303
+ screenToWorld,
10304
+ updateCanvuInteractionCurrent
10305
+ ]);
9947
10306
  const handleOverlayPointerDown = useCallback(
9948
10307
  (e) => {
9949
10308
  let currentDragState = dragStateRef.current;
@@ -9981,7 +10340,7 @@ var VectorViewport = forwardRef(
9981
10340
  if (e.button !== 0) return;
9982
10341
  if (editingTextIdRef.current) return;
9983
10342
  wrapperRef.current?.focus({ preventScroll: true });
9984
- setContextMenu(null);
10343
+ setContextMenuState(null);
9985
10344
  const tool = toolIdRef.current;
9986
10345
  if (tool === "hand") return;
9987
10346
  if (applePencilNav && e.pointerType === "pen" && navigator.maxTouchPoints > 0 && (tool === "draw" || tool === "marker")) {
@@ -10122,7 +10481,7 @@ var VectorViewport = forwardRef(
10122
10481
  ignoreLocked: true
10123
10482
  });
10124
10483
  if (hit) {
10125
- const selectModeClickPlacement = !e.shiftKey ? findSelectModeItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10484
+ const selectModeClickPlacement = !e.shiftKey ? findReadOnlyItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10126
10485
  if (selectModeClickPlacement) {
10127
10486
  const isAlreadySelected = cur.includes(hit.id);
10128
10487
  const moveIds = isAlreadySelected ? [...cur] : [hit.id];
@@ -10401,7 +10760,7 @@ var VectorViewport = forwardRef(
10401
10760
  return;
10402
10761
  }
10403
10762
  wrapperRef.current?.focus({ preventScroll: true });
10404
- setContextMenu(null);
10763
+ setContextMenuState(null);
10405
10764
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10406
10765
  const itemId = createShapeId();
10407
10766
  const item = createFreehandStrokeItem(
@@ -10524,7 +10883,7 @@ var VectorViewport = forwardRef(
10524
10883
  return;
10525
10884
  }
10526
10885
  wrapperRef.current?.focus({ preventScroll: true });
10527
- setContextMenu(null);
10886
+ setContextMenuState(null);
10528
10887
  penDetectedRef.current = true;
10529
10888
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10530
10889
  const itemId = createShapeId();
@@ -11029,7 +11388,7 @@ var VectorViewport = forwardRef(
11029
11388
  });
11030
11389
  return;
11031
11390
  }
11032
- const placement = findSelectModeItemClickPlacement(
11391
+ const placement = findReadOnlyItemClickPlacement(
11033
11392
  item,
11034
11393
  allCustomPlacementsRef.current
11035
11394
  );
@@ -11040,27 +11399,9 @@ var VectorViewport = forwardRef(
11040
11399
  });
11041
11400
  return;
11042
11401
  }
11043
- const { worldX, worldY } = currentWorld;
11044
- onSelectModeItemClick({
11045
- item,
11046
- worldX,
11047
- worldY,
11048
- clientX: ev.clientX,
11049
- clientY: ev.clientY,
11050
- pointerType: ev.pointerType,
11051
- shiftKey: ev.shiftKey,
11052
- altKey: ev.altKey,
11053
- metaKey: ev.metaKey,
11054
- ctrlKey: ev.ctrlKey,
11055
- items: itemsRef.current,
11056
- updateItem: (next) => {
11057
- onItemsChangeRef.current?.(
11058
- replaceItem(itemsRef.current, item.id, next),
11059
- { motive: "custom", itemIds: [item.id] }
11060
- );
11061
- },
11062
- setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
11063
- });
11402
+ onSelectModeItemClick(
11403
+ buildSelectModeItemClickDetail(item, currentWorld, ev)
11404
+ );
11064
11405
  finishCanvuInteraction("completed", {
11065
11406
  current: currentInteractionPoint,
11066
11407
  info: { motive: "custom", itemIds: [item.id], toolId: "select" }
@@ -11430,6 +11771,7 @@ var VectorViewport = forwardRef(
11430
11771
  document.removeEventListener("pointercancel", onUp);
11431
11772
  };
11432
11773
  }, [
11774
+ buildSelectModeItemClickDetail,
11433
11775
  emitRemoteStrokePreview,
11434
11776
  emitRemoteStrokePreviewClear,
11435
11777
  interactive,
@@ -11581,6 +11923,66 @@ var VectorViewport = forwardRef(
11581
11923
  const defaultNavMenu = cameraForOverlay ? /* @__PURE__ */ jsx(NavMenu, {}) : null;
11582
11924
  const resolvedSelectionInspector = selectionInspector === void 0 ? defaultSelectionInspector : selectionInspector;
11583
11925
  const resolvedNavMenu = navMenu === void 0 ? defaultNavMenu : navMenu;
11926
+ const contextMenuItemIds = contextMenuState?.itemIds ?? [];
11927
+ const contextMenuSelectedItems = contextMenuState ? contextMenuItemIds.map((id) => items.find((item) => item.id === id)).filter((item) => item != null) : [];
11928
+ const allContextMenuSelectedLocked = contextMenuItemIds.length > 0 && contextMenuItemIds.every((id) => items.find((item) => item.id === id)?.locked);
11929
+ const closeContextMenu = () => setContextMenuState(null);
11930
+ const runContextMenuAction = (fn) => () => {
11931
+ fn();
11932
+ closeContextMenu();
11933
+ };
11934
+ const contextMenuRenderContext = contextMenuState ? {
11935
+ position: { x: contextMenuState.x, y: contextMenuState.y },
11936
+ selectedIds: contextMenuItemIds,
11937
+ selectedItems: contextMenuSelectedItems,
11938
+ allSelectedLocked: allContextMenuSelectedLocked,
11939
+ anySelectedLocked: contextMenuSelectedItems.some((item) => item.locked),
11940
+ close: closeContextMenu,
11941
+ cut: runContextMenuAction(() => cutIds(contextMenuItemIds)),
11942
+ copy: runContextMenuAction(
11943
+ () => copyIdsToInternalClipboard(contextMenuItemIds)
11944
+ ),
11945
+ duplicate: runContextMenuAction(() => duplicateIds(contextMenuItemIds)),
11946
+ toggleLock: runContextMenuAction(
11947
+ () => setLockedOnIds(contextMenuItemIds, !allContextMenuSelectedLocked)
11948
+ ),
11949
+ bringToFront: runContextMenuAction(
11950
+ () => reorderIds(contextMenuItemIds, "front")
11951
+ ),
11952
+ bringForward: runContextMenuAction(
11953
+ () => reorderIds(contextMenuItemIds, "forward")
11954
+ ),
11955
+ sendBackward: runContextMenuAction(
11956
+ () => reorderIds(contextMenuItemIds, "backward")
11957
+ ),
11958
+ sendToBack: runContextMenuAction(
11959
+ () => reorderIds(contextMenuItemIds, "back")
11960
+ ),
11961
+ delete: runContextMenuAction(() => deleteIds(contextMenuItemIds)),
11962
+ deleteSelection: runContextMenuAction(() => deleteIds(contextMenuItemIds)),
11963
+ items: resolvedContextMenuItems
11964
+ } : null;
11965
+ const resolvedContextMenu = interactive && onItemsChange && contextMenuState && contextMenuRenderContext && renderContextMenu !== null ? renderContextMenu ? renderContextMenu(contextMenuRenderContext) : /* @__PURE__ */ jsx(
11966
+ ShapeContextMenu,
11967
+ {
11968
+ x: contextMenuState.x,
11969
+ y: contextMenuState.y,
11970
+ selectedIds: contextMenuItemIds,
11971
+ selectedItems: contextMenuSelectedItems,
11972
+ allSelectedLocked: allContextMenuSelectedLocked,
11973
+ items: resolvedContextMenuItems,
11974
+ onClose: closeContextMenu,
11975
+ onToggleLock: () => setLockedOnIds(contextMenuItemIds, !allContextMenuSelectedLocked),
11976
+ onCut: () => cutIds(contextMenuItemIds),
11977
+ onCopy: () => copyIdsToInternalClipboard(contextMenuItemIds),
11978
+ onBringToFront: () => reorderIds(contextMenuItemIds, "front"),
11979
+ onBringForward: () => reorderIds(contextMenuItemIds, "forward"),
11980
+ onSendBackward: () => reorderIds(contextMenuItemIds, "backward"),
11981
+ onSendToBack: () => reorderIds(contextMenuItemIds, "back"),
11982
+ onDuplicate: () => duplicateIds(contextMenuItemIds),
11983
+ onDelete: () => deleteIds(contextMenuItemIds)
11984
+ }
11985
+ ) : null;
11584
11986
  return /* @__PURE__ */ jsx(CanvuPluginContext.Provider, { value: pluginContextValue, children: /* @__PURE__ */ jsx(CanvuChromeContext.Provider, { value: chromeContextValue, children: /* @__PURE__ */ jsxs(
11585
11987
  "div",
11586
11988
  {
@@ -11734,30 +12136,7 @@ var VectorViewport = forwardRef(
11734
12136
  }
11735
12137
  ),
11736
12138
  resolvedSelectionInspector,
11737
- interactive && onItemsChange && contextMenu && /* @__PURE__ */ jsx(
11738
- ShapeContextMenu,
11739
- {
11740
- x: contextMenu.x,
11741
- y: contextMenu.y,
11742
- allSelectedLocked: contextMenu.itemIds.length > 0 && contextMenu.itemIds.every(
11743
- (id) => items.find((i) => i.id === id)?.locked
11744
- ),
11745
- onClose: () => setContextMenu(null),
11746
- onToggleLock: () => {
11747
- const ids = contextMenu.itemIds;
11748
- const allLocked = ids.length > 0 && ids.every((id) => items.find((i) => i.id === id)?.locked);
11749
- setLockedOnIds(ids, !allLocked);
11750
- },
11751
- onCut: () => cutIds(contextMenu.itemIds),
11752
- onCopy: () => copyIdsToInternalClipboard(contextMenu.itemIds),
11753
- onBringToFront: () => reorderIds(contextMenu.itemIds, "front"),
11754
- onBringForward: () => reorderIds(contextMenu.itemIds, "forward"),
11755
- onSendBackward: () => reorderIds(contextMenu.itemIds, "backward"),
11756
- onSendToBack: () => reorderIds(contextMenu.itemIds, "back"),
11757
- onDuplicate: () => duplicateIds(contextMenu.itemIds),
11758
- onDelete: () => deleteIds(contextMenu.itemIds)
11759
- }
11760
- ),
12139
+ resolvedContextMenu,
11761
12140
  resolvedNavMenu,
11762
12141
  /* @__PURE__ */ jsx(
11763
12142
  "div",
@@ -11776,6 +12155,6 @@ var VectorViewport = forwardRef(
11776
12155
  );
11777
12156
  VectorViewport.displayName = "VectorViewport";
11778
12157
 
11779
- export { CanvuChromeContext, CanvuPluginContext, DEFAULT_OVERFLOW_TOOL_IDS, DEFAULT_VECTOR_CANVAS_STORAGE_KEY, DEFAULT_VECTOR_TOOLS, IconArchitecturalCloud, IconArrow, IconDraw, IconEllipse, IconHand, IconImage, IconLaser, IconLine, IconRect, IconSelect, IconText, ImagesMenu, NavMenu, ShapeContextMenu, VectorCanvas, VectorCanvasBody, VectorCanvasHeader, VectorCanvasMain, VectorCanvasRoot, VectorCanvasToolbar, VectorCanvasViewportSurface, VectorSelectionInspector, VectorToolbar, VectorViewport, createCanvuPlugin, createIndexedDbPersistenceAdapter, createLocalStoragePersistenceAdapter, createNoopPersistenceAdapter, createToolPlugin, cursorForVectorToolId, getBoardPositionStyle, hydrateSceneItemsWithAssets, ingestAssetFilesToSceneItems, useCanvuChromeContext, useCanvuDocumentContext, useCanvuPluginContext, useCanvuPluginContribution, useCanvuResolvedTools, useCanvuViewportContext, useVectorCanvasDocument };
12158
+ export { CanvuChromeContext, CanvuPluginContext, DEFAULT_OVERFLOW_TOOL_IDS, DEFAULT_VECTOR_CANVAS_STORAGE_KEY, DEFAULT_VECTOR_TOOLS, IconArchitecturalCloud, IconArrow, IconDraw, IconEllipse, IconHand, IconImage, IconLaser, IconLine, IconRect, IconSelect, IconText, ImagesMenu, NavMenu, SHAPE_CONTEXT_MENU_ITEM_IDS, ShapeContextMenu, VectorCanvas, VectorCanvasBody, VectorCanvasHeader, VectorCanvasMain, VectorCanvasRoot, VectorCanvasToolbar, VectorCanvasViewportSurface, VectorSelectionInspector, VectorToolbar, VectorViewport, createCanvuPlugin, createIndexedDbPersistenceAdapter, createLocalStoragePersistenceAdapter, createNoopPersistenceAdapter, createToolPlugin, cursorForVectorToolId, getBoardPositionStyle, hydrateSceneItemsWithAssets, ingestAssetFilesToSceneItems, useCanvuChromeContext, useCanvuDocumentContext, useCanvuPluginContext, useCanvuPluginContribution, useCanvuResolvedTools, useCanvuViewportContext, useVectorCanvasDocument };
11780
12159
  //# sourceMappingURL=react.js.map
11781
12160
  //# sourceMappingURL=react.js.map