@fieldnotes/react 0.13.0 → 0.14.1

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.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  forwardRef,
7
7
  useImperativeHandle
8
8
  } from "react";
9
- import { Viewport } from "@fieldnotes/core";
9
+ import { Viewport, createDefaultTools } from "@fieldnotes/core";
10
10
 
11
11
  // src/context.ts
12
12
  import { createContext } from "react";
@@ -21,6 +21,9 @@ var FieldNotesCanvas = forwardRef(
21
21
  defaultTool,
22
22
  tool,
23
23
  onToolChange,
24
+ initialData,
25
+ onChange,
26
+ changeDebounceMs = 250,
24
27
  snapToGrid,
25
28
  className,
26
29
  style,
@@ -29,23 +32,65 @@ var FieldNotesCanvas = forwardRef(
29
32
  }, ref) {
30
33
  const containerRef = useRef(null);
31
34
  const [viewport, setViewport] = useState(null);
35
+ const changeRef = useRef({ onChange, changeDebounceMs });
36
+ changeRef.current = { onChange, changeDebounceMs };
32
37
  useImperativeHandle(ref, () => ({ viewport }), [viewport]);
33
38
  useEffect(() => {
34
39
  const el = containerRef.current;
35
40
  if (!el) return;
36
41
  const vp = new Viewport(el, options);
37
- if (tools) {
38
- for (const t of tools) {
39
- vp.toolManager.register(t);
40
- }
42
+ const activate = (fallback) => {
43
+ const name = tool ?? defaultTool ?? fallback;
44
+ if (name) vp.setTool(name);
45
+ };
46
+ const deferDefaults = tools === void 0 && options?.tools === void 0;
47
+ for (const t of tools === "default" ? createDefaultTools() : tools ?? []) {
48
+ vp.toolManager.register(t);
41
49
  }
42
- const initialTool = tool ?? defaultTool;
43
- if (initialTool) {
44
- vp.setTool(initialTool);
50
+ if (!deferDefaults) activate(tools === "default" ? "select" : void 0);
51
+ try {
52
+ if (typeof initialData === "string") vp.loadJSON(initialData);
53
+ else if (initialData) vp.loadState(initialData);
54
+ } catch (error) {
55
+ vp.destroy();
56
+ throw error;
57
+ }
58
+ let timer;
59
+ const emit = () => {
60
+ timer = void 0;
61
+ changeRef.current.onChange?.(vp.exportState());
62
+ };
63
+ const unsubscribe = vp.onStateChange(() => {
64
+ clearTimeout(timer);
65
+ timer = setTimeout(emit, changeRef.current.changeDebounceMs);
66
+ });
67
+ try {
68
+ onReady?.(vp);
69
+ if (deferDefaults) {
70
+ const registerDefaults = vp.toolManager.toolNames.length === 0;
71
+ if (registerDefaults) {
72
+ for (const t of createDefaultTools()) vp.toolManager.register(t);
73
+ }
74
+ activate(registerDefaults ? "select" : void 0);
75
+ }
76
+ } catch (error) {
77
+ clearTimeout(timer);
78
+ unsubscribe();
79
+ vp.destroy();
80
+ throw error;
45
81
  }
46
82
  setViewport(vp);
47
- onReady?.(vp);
48
83
  return () => {
84
+ if (timer !== void 0) {
85
+ clearTimeout(timer);
86
+ try {
87
+ emit();
88
+ } catch (error) {
89
+ if (typeof reportError === "function") reportError(error);
90
+ else console.error("[fieldnotes] onChange failed during unmount", error);
91
+ }
92
+ }
93
+ unsubscribe();
49
94
  vp.destroy();
50
95
  setViewport(null);
51
96
  };
@@ -65,7 +110,7 @@ var FieldNotesCanvas = forwardRef(
65
110
  viewport.setSnapToGrid(snapToGrid);
66
111
  }, [viewport, snapToGrid]);
67
112
  useEffect(() => {
68
- if (!viewport || !tools) return;
113
+ if (!viewport || !Array.isArray(tools)) return;
69
114
  for (const t of tools) {
70
115
  if (!viewport.toolManager.getTool(t.name)) {
71
116
  viewport.toolManager.register(t);
@@ -113,12 +158,11 @@ function CanvasElement({ position, size, children }) {
113
158
  };
114
159
  elementIdRef.current = register();
115
160
  setPortalTarget(container);
116
- let disposed = false;
117
161
  const handleStoreReplacement = () => {
118
162
  const clearedId = elementIdRef.current;
119
163
  if (clearedId === null) return;
120
164
  queueMicrotask(() => {
121
- if (disposed || elementIdRef.current !== clearedId) return;
165
+ if (elementIdRef.current !== clearedId) return;
122
166
  if (viewport.store.getById(clearedId)?.type === "html") {
123
167
  viewport.updateHtmlElement(clearedId, container);
124
168
  } else {
@@ -130,7 +174,6 @@ function CanvasElement({ position, size, children }) {
130
174
  const unsubscribeClear = viewport.store.on("clear", handleStoreReplacement);
131
175
  const unsubscribeBatch = viewport.store.on("batch", handleStoreReplacement);
132
176
  return () => {
133
- disposed = true;
134
177
  unsubscribeClear();
135
178
  unsubscribeBatch();
136
179
  if (elementIdRef.current) {
@@ -154,67 +197,42 @@ function CanvasElement({ position, size, children }) {
154
197
  }
155
198
 
156
199
  // src/hooks/use-active-tool.ts
157
- import { useCallback, useSyncExternalStore } from "react";
200
+ import { useCallback, useMemo, useSyncExternalStore } from "react";
201
+ import { createActiveToolSource } from "@fieldnotes/core";
158
202
  function useActiveTool() {
159
203
  const viewport = useViewport();
160
- const subscribe = useCallback(
161
- (onStoreChange) => viewport.toolManager.onChange(onStoreChange),
162
- [viewport]
163
- );
164
- const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
165
- const toolName = useSyncExternalStore(subscribe, getSnapshot);
204
+ const source = useMemo(() => createActiveToolSource(viewport), [viewport]);
205
+ const toolName = useSyncExternalStore(source.subscribe, source.getSnapshot);
166
206
  const setTool = useCallback((name) => viewport.setTool(name), [viewport]);
167
207
  return [toolName, setTool];
168
208
  }
169
209
 
170
210
  // src/hooks/use-camera.ts
171
- import { useCallback as useCallback2, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
211
+ import { useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
212
+ import { createCameraSource } from "@fieldnotes/core";
172
213
  function useCamera() {
173
214
  const viewport = useViewport();
174
- const cachedRef = useRef3({ x: 0, y: 0, zoom: 1 });
175
- const subscribe = useCallback2(
176
- (onStoreChange) => viewport.camera.onChange(onStoreChange),
177
- [viewport]
178
- );
179
- const getSnapshot = useCallback2(() => {
180
- const { position, zoom } = viewport.camera;
181
- const cached = cachedRef.current;
182
- if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
183
- return cached;
184
- }
185
- const next = { x: position.x, y: position.y, zoom };
186
- cachedRef.current = next;
187
- return next;
188
- }, [viewport]);
189
- return useSyncExternalStore2(subscribe, getSnapshot);
215
+ const source = useMemo2(() => createCameraSource(viewport), [viewport]);
216
+ return useSyncExternalStore2(source.subscribe, source.getSnapshot);
190
217
  }
191
218
 
192
219
  // src/hooks/use-tool-options.ts
193
- import { useCallback as useCallback3, useRef as useRef4, useSyncExternalStore as useSyncExternalStore3 } from "react";
194
- function noop() {
195
- }
220
+ import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore3 } from "react";
221
+ import { createToolOptionsSource } from "@fieldnotes/core";
196
222
  function useToolOptions(toolName) {
197
223
  const viewport = useViewport();
198
- const cachedRef = useRef4(null);
199
- const subscribe = useCallback3(
200
- (onStoreChange) => {
201
- const tool = viewport.toolManager.getTool(toolName);
202
- if (!tool?.onOptionsChange) return noop;
203
- return tool.onOptionsChange(onStoreChange);
204
- },
205
- [viewport, toolName]
224
+ const nameRef = useRef3(toolName);
225
+ nameRef.current = toolName;
226
+ const source = useMemo3(
227
+ () => createToolOptionsSource(viewport, () => nameRef.current),
228
+ [viewport]
206
229
  );
207
- const getSnapshot = useCallback3(() => {
208
- const tool = viewport.toolManager.getTool(toolName);
209
- if (!tool?.getOptions) return null;
210
- const next = tool.getOptions();
211
- const cached = cachedRef.current;
212
- if (cached && shallowEqual(cached, next)) return cached;
213
- cachedRef.current = next;
214
- return next;
215
- }, [viewport, toolName]);
216
- const options = useSyncExternalStore3(subscribe, getSnapshot);
217
- const setOptions = useCallback3(
230
+ const subscribe = useCallback2(
231
+ (onStoreChange) => source.subscribe(onStoreChange),
232
+ [source, toolName]
233
+ );
234
+ const options = useSyncExternalStore3(subscribe, source.getSnapshot);
235
+ const setOptions = useCallback2(
218
236
  (partial) => {
219
237
  const tool = viewport.toolManager.getTool(toolName);
220
238
  tool?.setOptions?.(partial);
@@ -223,84 +241,47 @@ function useToolOptions(toolName) {
223
241
  );
224
242
  return [options, setOptions];
225
243
  }
226
- function shallowEqual(a, b) {
227
- const keysA = Object.keys(a);
228
- const keysB = Object.keys(b);
229
- if (keysA.length !== keysB.length) return false;
230
- for (const key of keysA) {
231
- const valA = a[key];
232
- const valB = b[key];
233
- if (valA === valB) continue;
234
- if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
235
- const objA = valA;
236
- const objB = valB;
237
- const innerKeys = Object.keys(objA);
238
- if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
239
- continue;
240
- }
241
- }
242
- return false;
243
- }
244
- return true;
245
- }
246
244
 
247
245
  // src/hooks/use-layers.ts
248
- import { useCallback as useCallback4, useRef as useRef5, useSyncExternalStore as useSyncExternalStore4 } from "react";
246
+ import { useCallback as useCallback3, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore4 } from "react";
247
+ import { createLayersSource } from "@fieldnotes/core";
249
248
  function useLayers() {
250
249
  const viewport = useViewport();
251
- const cachedRef = useRef5({ layers: [], activeLayerId: "" });
252
- const subscribe = useCallback4(
253
- (onStoreChange) => viewport.layerManager.on("change", onStoreChange),
254
- [viewport]
255
- );
256
- const getSnapshot = useCallback4(() => {
257
- const layers = viewport.layerManager.getLayers();
258
- const activeLayerId = viewport.layerManager.activeLayerId;
259
- const cached = cachedRef.current;
260
- if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
261
- const next2 = layers[i];
262
- return next2 !== void 0 && l.id === next2.id && l.name === next2.name && l.visible === next2.visible && l.locked === next2.locked && l.order === next2.order && l.opacity === next2.opacity;
263
- })) {
264
- return cached;
265
- }
266
- const next = { layers, activeLayerId };
267
- cachedRef.current = next;
268
- return next;
269
- }, [viewport]);
270
- const snapshot = useSyncExternalStore4(subscribe, getSnapshot);
271
- const createLayer = useCallback4(
250
+ const source = useMemo4(() => createLayersSource(viewport), [viewport]);
251
+ const snapshot = useSyncExternalStore4(source.subscribe, source.getSnapshot);
252
+ const createLayer = useCallback3(
272
253
  (name) => viewport.layerManager.createLayer(name),
273
254
  [viewport]
274
255
  );
275
- const removeLayer = useCallback4(
256
+ const removeLayer = useCallback3(
276
257
  (id) => viewport.layerManager.removeLayer(id),
277
258
  [viewport]
278
259
  );
279
- const renameLayer = useCallback4(
260
+ const renameLayer = useCallback3(
280
261
  (id, name) => viewport.layerManager.renameLayer(id, name),
281
262
  [viewport]
282
263
  );
283
- const reorderLayer = useCallback4(
264
+ const reorderLayer = useCallback3(
284
265
  (id, newOrder) => viewport.layerManager.reorderLayer(id, newOrder),
285
266
  [viewport]
286
267
  );
287
- const setVisible = useCallback4(
268
+ const setVisible = useCallback3(
288
269
  (id, visible) => viewport.layerManager.setLayerVisible(id, visible),
289
270
  [viewport]
290
271
  );
291
- const setLocked = useCallback4(
272
+ const setLocked = useCallback3(
292
273
  (id, locked) => viewport.layerManager.setLayerLocked(id, locked),
293
274
  [viewport]
294
275
  );
295
- const setOpacity = useCallback4(
276
+ const setOpacity = useCallback3(
296
277
  (id, opacity) => viewport.layerManager.setLayerOpacity(id, opacity),
297
278
  [viewport]
298
279
  );
299
- const setActiveLayer = useCallback4(
280
+ const setActiveLayer = useCallback3(
300
281
  (id) => viewport.layerManager.setActiveLayer(id),
301
282
  [viewport]
302
283
  );
303
- const moveElement = useCallback4(
284
+ const moveElement = useCallback3(
304
285
  (elementId, layerId) => viewport.layerManager.moveElementToLayer(elementId, layerId),
305
286
  [viewport]
306
287
  );
@@ -320,28 +301,14 @@ function useLayers() {
320
301
  }
321
302
 
322
303
  // src/hooks/use-history.ts
323
- import { useCallback as useCallback5, useRef as useRef6, useSyncExternalStore as useSyncExternalStore5 } from "react";
304
+ import { useCallback as useCallback4, useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore5 } from "react";
305
+ import { createHistorySource } from "@fieldnotes/core";
324
306
  function useHistory() {
325
307
  const viewport = useViewport();
326
- const cachedRef = useRef6({ canUndo: false, canRedo: false });
327
- const subscribe = useCallback5(
328
- (onStoreChange) => viewport.history.onChange(onStoreChange),
329
- [viewport]
330
- );
331
- const getSnapshot = useCallback5(() => {
332
- const canUndo = viewport.history.canUndo;
333
- const canRedo = viewport.history.canRedo;
334
- const cached = cachedRef.current;
335
- if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
336
- return cached;
337
- }
338
- const next = { canUndo, canRedo };
339
- cachedRef.current = next;
340
- return next;
341
- }, [viewport]);
342
- const snapshot = useSyncExternalStore5(subscribe, getSnapshot);
343
- const undo = useCallback5(() => viewport.undo(), [viewport]);
344
- const redo = useCallback5(() => viewport.redo(), [viewport]);
308
+ const source = useMemo5(() => createHistorySource(viewport), [viewport]);
309
+ const snapshot = useSyncExternalStore5(source.subscribe, source.getSnapshot);
310
+ const undo = useCallback4(() => viewport.undo(), [viewport]);
311
+ const redo = useCallback4(() => viewport.redo(), [viewport]);
345
312
  return {
346
313
  canUndo: snapshot.canUndo,
347
314
  canRedo: snapshot.canRedo,
@@ -351,197 +318,80 @@ function useHistory() {
351
318
  }
352
319
 
353
320
  // src/hooks/use-elements.ts
354
- import { useCallback as useCallback6, useRef as useRef7, useSyncExternalStore as useSyncExternalStore6 } from "react";
355
- function shallowEqual2(a, b) {
356
- if (Object.is(a, b)) return true;
357
- if (Array.isArray(a) && Array.isArray(b)) {
358
- return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
359
- }
360
- if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
361
- const ka = Object.keys(a);
362
- const kb = Object.keys(b);
363
- return ka.length === kb.length && ka.every(
364
- (k) => k in b && Object.is(a[k], b[k])
365
- );
366
- }
367
- return false;
368
- }
321
+ import { useMemo as useMemo6, useRef as useRef4, useSyncExternalStore as useSyncExternalStore6 } from "react";
322
+ import { createElementsSelectorSource, createElementsSource } from "@fieldnotes/core";
369
323
  function useElements(arg, isEqual) {
370
324
  const viewport = useViewport();
371
- const cachedRef = useRef7([]);
372
- const selectedRef = useRef7(null);
373
- const subscribe = useCallback6(
374
- (onStoreChange) => viewport.store.onChange(onStoreChange),
325
+ const latest = useRef4({ arg, isEqual });
326
+ latest.current = { arg, isEqual };
327
+ const elementsSource = useMemo6(
328
+ () => createElementsSource(viewport, () => {
329
+ const a = latest.current.arg;
330
+ return typeof a === "function" ? void 0 : a;
331
+ }),
375
332
  [viewport]
376
333
  );
377
- const getSnapshot = useCallback6(() => {
378
- if (typeof arg === "function") {
379
- const next2 = arg(viewport.store.getAll());
380
- const prev = selectedRef.current;
381
- const equal = isEqual ?? shallowEqual2;
382
- if (prev !== null && equal(prev.value, next2)) {
383
- return prev.value;
384
- }
385
- selectedRef.current = { value: next2 };
386
- return next2;
387
- }
388
- const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
389
- const cached = cachedRef.current;
390
- if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
391
- return cached;
392
- }
393
- cachedRef.current = next;
394
- return next;
395
- }, [viewport, arg, isEqual]);
396
- return useSyncExternalStore6(subscribe, getSnapshot);
334
+ const selectorSource = useMemo6(
335
+ () => createElementsSelectorSource(
336
+ viewport,
337
+ // Only read while `arg` is a selector: the source is picked below by `typeof arg`.
338
+ () => latest.current.arg,
339
+ () => latest.current.isEqual
340
+ ),
341
+ [viewport]
342
+ );
343
+ const source = typeof arg === "function" ? selectorSource : elementsSource;
344
+ return useSyncExternalStore6(source.subscribe, source.getSnapshot);
397
345
  }
398
346
 
399
347
  // src/hooks/use-selection.ts
400
- import { useCallback as useCallback7, useSyncExternalStore as useSyncExternalStore7 } from "react";
348
+ import { useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore7 } from "react";
349
+ import { createSelectionSource } from "@fieldnotes/core";
401
350
  function useSelection() {
402
351
  const viewport = useViewport();
403
- const subscribe = useCallback7(
404
- (onChange) => viewport.onSelectionChange(onChange),
405
- [viewport]
406
- );
407
- const getSnapshot = useCallback7(() => viewport.getSelectedIds(), [viewport]);
408
- return useSyncExternalStore7(subscribe, getSnapshot, getSnapshot);
352
+ const source = useMemo7(() => createSelectionSource(viewport), [viewport]);
353
+ return useSyncExternalStore7(source.subscribe, source.getSnapshot, source.getSnapshot);
409
354
  }
410
355
 
411
356
  // src/hooks/use-selection-style.ts
412
- import { useCallback as useCallback8, useRef as useRef8, useSyncExternalStore as useSyncExternalStore8 } from "react";
413
-
414
- // src/hooks/style-keys.ts
415
- var STYLE_KEYS = [
416
- "color",
417
- "fillColor",
418
- "strokeWidth",
419
- "opacity",
420
- "fontSize",
421
- "strokeStyle"
422
- ];
423
-
424
- // src/hooks/use-selection-style.ts
425
- function styleEqual(a, b) {
426
- if (a === b) return true;
427
- if (a === null || b === null) return false;
428
- return STYLE_KEYS.every((k) => a[k] === b[k]);
429
- }
357
+ import { useCallback as useCallback5, useMemo as useMemo8, useSyncExternalStore as useSyncExternalStore8 } from "react";
358
+ import { createSelectionStyleSource } from "@fieldnotes/core";
430
359
  function useSelectionStyle() {
431
360
  const viewport = useViewport();
432
- const cacheRef = useRef8(null);
433
- const subscribe = useCallback8(
434
- (onChange) => {
435
- const offSel = viewport.onSelectionChange(onChange);
436
- const offStore = viewport.store.onChange(onChange);
437
- return () => {
438
- offSel();
439
- offStore();
440
- };
441
- },
442
- [viewport]
443
- );
444
- const getSnapshot = useCallback8(() => {
445
- const next = viewport.getSelectionStyle();
446
- if (styleEqual(cacheRef.current, next)) return cacheRef.current;
447
- cacheRef.current = next;
448
- return next;
449
- }, [viewport]);
450
- const style = useSyncExternalStore8(subscribe, getSnapshot, getSnapshot);
451
- const apply = useCallback8((s) => viewport.applyStyleToSelection(s), [viewport]);
361
+ const source = useMemo8(() => createSelectionStyleSource(viewport), [viewport]);
362
+ const style = useSyncExternalStore8(source.subscribe, source.getSnapshot, source.getSnapshot);
363
+ const apply = useCallback5((s) => viewport.applyStyleToSelection(s), [viewport]);
452
364
  return [style, apply];
453
365
  }
454
366
 
455
367
  // src/hooks/use-selection-style-details.ts
456
- import { useCallback as useCallback9, useRef as useRef9, useSyncExternalStore as useSyncExternalStore9 } from "react";
457
- function keysEqual(a, b) {
458
- return a.length === b.length && a.every((k, i) => k === b[i]);
459
- }
460
- function detailsEqual(a, b) {
461
- if (a === b) return true;
462
- if (a === null || b === null) return false;
463
- return keysEqual(a.applicable, b.applicable) && keysEqual(a.mixed, b.mixed) && STYLE_KEYS.every((k) => a.common[k] === b.common[k]);
464
- }
368
+ import { useCallback as useCallback6, useMemo as useMemo9, useSyncExternalStore as useSyncExternalStore9 } from "react";
369
+ import { createSelectionStyleDetailsSource } from "@fieldnotes/core";
465
370
  function useSelectionStyleDetails() {
466
371
  const viewport = useViewport();
467
- const cacheRef = useRef9(null);
468
- const subscribe = useCallback9(
469
- (onChange) => {
470
- const offSel = viewport.onSelectionChange(onChange);
471
- const offStore = viewport.store.onChange(onChange);
472
- return () => {
473
- offSel();
474
- offStore();
475
- };
476
- },
477
- [viewport]
478
- );
479
- const getSnapshot = useCallback9(() => {
480
- const next = viewport.getSelectionStyleDetails();
481
- if (detailsEqual(cacheRef.current, next)) return cacheRef.current;
482
- cacheRef.current = next;
483
- return next;
484
- }, [viewport]);
485
- const details = useSyncExternalStore9(subscribe, getSnapshot, getSnapshot);
486
- const apply = useCallback9((s) => viewport.applyStyleToSelection(s), [viewport]);
372
+ const source = useMemo9(() => createSelectionStyleDetailsSource(viewport), [viewport]);
373
+ const details = useSyncExternalStore9(source.subscribe, source.getSnapshot, source.getSnapshot);
374
+ const apply = useCallback6((s) => viewport.applyStyleToSelection(s), [viewport]);
487
375
  return [details, apply];
488
376
  }
489
377
 
490
378
  // src/hooks/use-selection-ops.ts
491
- import { useCallback as useCallback10, useRef as useRef10, useSyncExternalStore as useSyncExternalStore10 } from "react";
492
- function snapshotEqual(a, b) {
493
- return a.selectedIds === b.selectedIds && a.canGroup === b.canGroup && a.canUngroup === b.canUngroup && a.canAlign === b.canAlign && a.canDistribute === b.canDistribute && a.isLocked === b.isLocked;
494
- }
379
+ import { useCallback as useCallback7, useMemo as useMemo10, useSyncExternalStore as useSyncExternalStore10 } from "react";
380
+ import { createSelectionOpsSource } from "@fieldnotes/core";
495
381
  function useSelectionOps() {
496
382
  const viewport = useViewport();
497
- const cacheRef = useRef10(null);
498
- const subscribe = useCallback10(
499
- (onChange) => {
500
- const offSel = viewport.onSelectionChange(onChange);
501
- const offStore = viewport.store.onChange(onChange);
502
- return () => {
503
- offSel();
504
- offStore();
505
- };
506
- },
507
- [viewport]
508
- );
509
- const getSnapshot = useCallback10(() => {
510
- const ids = viewport.getSelectedIds();
511
- const n = ids.length;
512
- let canUngroup = false;
513
- let lockedCount = 0;
514
- for (const id of ids) {
515
- const el = viewport.store.getById(id);
516
- if (!el) continue;
517
- if (el.groupId !== void 0) canUngroup = true;
518
- if (el.locked) lockedCount++;
519
- }
520
- const isLocked = n === 0 ? null : lockedCount === n ? true : lockedCount === 0 ? false : null;
521
- const next = {
522
- selectedIds: ids,
523
- canGroup: n >= 2,
524
- canUngroup,
525
- canAlign: n >= 2,
526
- canDistribute: n >= 3,
527
- isLocked
528
- };
529
- const prev = cacheRef.current;
530
- if (prev && snapshotEqual(prev, next)) return prev;
531
- cacheRef.current = next;
532
- return next;
533
- }, [viewport]);
534
- const snap = useSyncExternalStore10(subscribe, getSnapshot, getSnapshot);
535
- const group = useCallback10(() => viewport.groupSelection(), [viewport]);
536
- const ungroup = useCallback10(() => viewport.ungroupSelection(), [viewport]);
537
- const toggleLock = useCallback10(() => viewport.toggleLockSelection(), [viewport]);
538
- const align = useCallback10((edge) => viewport.alignSelection(edge), [viewport]);
539
- const distribute = useCallback10(
383
+ const source = useMemo10(() => createSelectionOpsSource(viewport), [viewport]);
384
+ const snap = useSyncExternalStore10(source.subscribe, source.getSnapshot, source.getSnapshot);
385
+ const group = useCallback7(() => viewport.groupSelection(), [viewport]);
386
+ const ungroup = useCallback7(() => viewport.ungroupSelection(), [viewport]);
387
+ const toggleLock = useCallback7(() => viewport.toggleLockSelection(), [viewport]);
388
+ const align = useCallback7((edge) => viewport.alignSelection(edge), [viewport]);
389
+ const distribute = useCallback7(
540
390
  (axis) => viewport.distributeSelection(axis),
541
391
  [viewport]
542
392
  );
543
- const rotateCW = useCallback10(() => viewport.rotateSelection("cw"), [viewport]);
544
- const rotateCCW = useCallback10(() => viewport.rotateSelection("ccw"), [viewport]);
393
+ const rotateCW = useCallback7(() => viewport.rotateSelection("cw"), [viewport]);
394
+ const rotateCCW = useCallback7(() => viewport.rotateSelection("ccw"), [viewport]);
545
395
  return {
546
396
  selectedIds: snap.selectedIds,
547
397
  selectedCount: snap.selectedIds.length,
@@ -561,16 +411,16 @@ function useSelectionOps() {
561
411
  }
562
412
 
563
413
  // src/hooks/use-element-rects.ts
564
- import { useCallback as useCallback11, useEffect as useEffect3, useRef as useRef11, useSyncExternalStore as useSyncExternalStore11 } from "react";
414
+ import { useCallback as useCallback8, useEffect as useEffect3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore11 } from "react";
565
415
  import { ElementRectTracker, computeElementRects, elementRectsEqual } from "@fieldnotes/core";
566
416
  function useElementRects(match) {
567
417
  const viewport = useViewport();
568
- const matchRef = useRef11(match);
418
+ const matchRef = useRef5(match);
569
419
  matchRef.current = match;
570
- const stableMatch = useCallback11((el) => matchRef.current(el), []);
571
- const cacheRef = useRef11(null);
572
- const trackerRef = useRef11(null);
573
- const subscribe = useCallback11(
420
+ const stableMatch = useCallback8((el) => matchRef.current(el), []);
421
+ const cacheRef = useRef5(null);
422
+ const trackerRef = useRef5(null);
423
+ const subscribe = useCallback8(
574
424
  (onStoreChange) => {
575
425
  const tracker = new ElementRectTracker(viewport, { match: stableMatch });
576
426
  trackerRef.current = tracker;
@@ -592,7 +442,7 @@ function useElementRects(match) {
592
442
  },
593
443
  [viewport, stableMatch]
594
444
  );
595
- const getSnapshot = useCallback11(() => {
445
+ const getSnapshot = useCallback8(() => {
596
446
  if (cacheRef.current === null) {
597
447
  cacheRef.current = computeElementRects(viewport.store, stableMatch);
598
448
  }
@@ -606,7 +456,7 @@ function useElementRects(match) {
606
456
  }
607
457
 
608
458
  // src/minimap.tsx
609
- import { useEffect as useEffect4, useRef as useRef12, useState as useState3 } from "react";
459
+ import { useEffect as useEffect4, useRef as useRef6, useState as useState3 } from "react";
610
460
  import { MinimapController } from "@fieldnotes/core";
611
461
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
612
462
  var toggleStyle = {
@@ -634,7 +484,7 @@ function Minimap({
634
484
  }) {
635
485
  const viewport = useViewport();
636
486
  const [collapsed, setCollapsed] = useState3(defaultCollapsed);
637
- const canvasRef = useRef12(null);
487
+ const canvasRef = useRef6(null);
638
488
  useEffect4(() => {
639
489
  if (collapsed) return;
640
490
  const canvas = canvasRef.current;