@fieldnotes/react 0.14.0 → 0.14.2

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/README.md CHANGED
@@ -8,7 +8,7 @@ React bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes
8
8
  npm install @fieldnotes/core @fieldnotes/react
9
9
  ```
10
10
 
11
- Requires React 18+.
11
+ Requires React 18+. Using Vue or Svelte? See [`@fieldnotes/vue`](../vue) and [`@fieldnotes/svelte`](../svelte).
12
12
 
13
13
  ## Quick Start
14
14
 
package/dist/index.cjs CHANGED
@@ -67,74 +67,37 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
67
67
  }, ref) {
68
68
  const containerRef = (0, import_react2.useRef)(null);
69
69
  const [viewport, setViewport] = (0, import_react2.useState)(null);
70
+ const mountedRef = (0, import_react2.useRef)(null);
70
71
  const changeRef = (0, import_react2.useRef)({ onChange, changeDebounceMs });
71
72
  changeRef.current = { onChange, changeDebounceMs };
72
73
  (0, import_react2.useImperativeHandle)(ref, () => ({ viewport }), [viewport]);
73
74
  (0, import_react2.useEffect)(() => {
74
75
  const el = containerRef.current;
75
76
  if (!el) return;
76
- const vp = new import_core.Viewport(el, options);
77
- const activate = (fallback) => {
78
- const name = tool ?? defaultTool ?? fallback;
79
- if (name) vp.setTool(name);
80
- };
81
- const deferDefaults = tools === void 0 && options?.tools === void 0;
82
- for (const t of tools === "default" ? (0, import_core.createDefaultTools)() : tools ?? []) {
83
- vp.toolManager.register(t);
84
- }
85
- if (!deferDefaults) activate(tools === "default" ? "select" : void 0);
86
- try {
87
- if (typeof initialData === "string") vp.loadJSON(initialData);
88
- else if (initialData) vp.loadState(initialData);
89
- } catch (error) {
90
- vp.destroy();
91
- throw error;
92
- }
93
- let timer;
94
- const emit = () => {
95
- timer = void 0;
96
- changeRef.current.onChange?.(vp.exportState());
97
- };
98
- const unsubscribe = vp.onStateChange(() => {
99
- clearTimeout(timer);
100
- timer = setTimeout(emit, changeRef.current.changeDebounceMs);
101
- });
102
- try {
103
- onReady?.(vp);
104
- if (deferDefaults) {
105
- const registerDefaults = vp.toolManager.toolNames.length === 0;
106
- if (registerDefaults) {
107
- for (const t of (0, import_core.createDefaultTools)()) vp.toolManager.register(t);
108
- }
109
- activate(registerDefaults ? "select" : void 0);
77
+ const mounted = (0, import_core.mountViewport)(el, {
78
+ options,
79
+ tools,
80
+ initialData,
81
+ tool,
82
+ defaultTool,
83
+ onReady,
84
+ get onChange() {
85
+ return changeRef.current.onChange;
86
+ },
87
+ get changeDebounceMs() {
88
+ return changeRef.current.changeDebounceMs;
110
89
  }
111
- } catch (error) {
112
- clearTimeout(timer);
113
- unsubscribe();
114
- vp.destroy();
115
- throw error;
116
- }
117
- setViewport(vp);
90
+ });
91
+ mountedRef.current = mounted;
92
+ setViewport(mounted.viewport);
118
93
  return () => {
119
- if (timer !== void 0) {
120
- clearTimeout(timer);
121
- try {
122
- emit();
123
- } catch (error) {
124
- if (typeof reportError === "function") reportError(error);
125
- else console.error("[fieldnotes] onChange failed during unmount", error);
126
- }
127
- }
128
- unsubscribe();
129
- vp.destroy();
94
+ mounted.destroy();
130
95
  setViewport(null);
131
96
  };
132
97
  }, []);
133
98
  (0, import_react2.useEffect)(() => {
134
99
  if (!viewport || tool === void 0) return;
135
- if (viewport.toolManager.activeTool?.name !== tool) {
136
- viewport.setTool(tool);
137
- }
100
+ mountedRef.current?.setTool(tool);
138
101
  }, [viewport, tool]);
139
102
  (0, import_react2.useEffect)(() => {
140
103
  if (!viewport || !onToolChange) return;
@@ -146,11 +109,7 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
146
109
  }, [viewport, snapToGrid]);
147
110
  (0, import_react2.useEffect)(() => {
148
111
  if (!viewport || !Array.isArray(tools)) return;
149
- for (const t of tools) {
150
- if (!viewport.toolManager.getTool(t.name)) {
151
- viewport.toolManager.register(t);
152
- }
153
- }
112
+ mountedRef.current?.addTools(tools);
154
113
  }, [viewport, tools]);
155
114
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: containerRef, className, style, children: viewport && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewportContext.Provider, { value: viewport, children }) });
156
115
  }
@@ -159,6 +118,7 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
159
118
  // src/canvas-element.tsx
160
119
  var import_react4 = require("react");
161
120
  var import_react_dom = require("react-dom");
121
+ var import_core2 = require("@fieldnotes/core");
162
122
 
163
123
  // src/hooks/use-viewport.ts
164
124
  var import_react3 = require("react");
@@ -171,11 +131,10 @@ function useViewport() {
171
131
  }
172
132
 
173
133
  // src/canvas-element.tsx
174
- var HOST_ORIGIN = "host";
175
134
  function CanvasElement({ position, size, children }) {
176
135
  const viewport = useViewport();
177
136
  const [portalTarget, setPortalTarget] = (0, import_react4.useState)(null);
178
- const elementIdRef = (0, import_react4.useRef)(null);
137
+ const hostRef = (0, import_react4.useRef)(null);
179
138
  const geometryRef = (0, import_react4.useRef)({ position, size });
180
139
  (0, import_react4.useEffect)(() => {
181
140
  const container = document.createElement("div");
@@ -183,51 +142,17 @@ function CanvasElement({ position, size, children }) {
183
142
  width: "100%",
184
143
  height: "100%"
185
144
  });
186
- const register = () => {
187
- viewport.domLayer.appendChild(container);
188
- const { position: current, size: currentSize } = geometryRef.current;
189
- return viewport.addHtmlElement(container, current, currentSize, {
190
- transient: true,
191
- origin: HOST_ORIGIN
192
- });
193
- };
194
- elementIdRef.current = register();
145
+ const host = (0, import_core2.attachHostElement)(viewport, container, geometryRef.current);
146
+ hostRef.current = host;
195
147
  setPortalTarget(container);
196
- let disposed = false;
197
- const handleStoreReplacement = () => {
198
- const clearedId = elementIdRef.current;
199
- if (clearedId === null) return;
200
- queueMicrotask(() => {
201
- if (disposed || elementIdRef.current !== clearedId) return;
202
- if (viewport.store.getById(clearedId)?.type === "html") {
203
- viewport.updateHtmlElement(clearedId, container);
204
- } else {
205
- elementIdRef.current = register();
206
- }
207
- viewport.requestRender();
208
- });
209
- };
210
- const unsubscribeClear = viewport.store.on("clear", handleStoreReplacement);
211
- const unsubscribeBatch = viewport.store.on("batch", handleStoreReplacement);
212
148
  return () => {
213
- disposed = true;
214
- unsubscribeClear();
215
- unsubscribeBatch();
216
- if (elementIdRef.current) {
217
- viewport.store.remove(elementIdRef.current, { origin: HOST_ORIGIN });
218
- viewport.requestRender();
219
- elementIdRef.current = null;
220
- }
221
- if (container.parentNode === viewport.domLayer) container.remove();
149
+ host.destroy();
222
150
  setPortalTarget(null);
223
151
  };
224
152
  }, [viewport]);
225
153
  (0, import_react4.useEffect)(() => {
226
154
  geometryRef.current = { position, size };
227
- const id = elementIdRef.current;
228
- if (!id) return;
229
- viewport.store.update(id, { position, ...size ? { size } : {} }, { origin: HOST_ORIGIN });
230
- viewport.requestRender();
155
+ hostRef.current?.setGeometry({ position, size });
231
156
  }, [viewport, position.x, position.y, size?.w, size?.h]);
232
157
  if (!portalTarget) return null;
233
158
  return (0, import_react_dom.createPortal)(children, portalTarget);
@@ -235,65 +160,40 @@ function CanvasElement({ position, size, children }) {
235
160
 
236
161
  // src/hooks/use-active-tool.ts
237
162
  var import_react5 = require("react");
163
+ var import_core3 = require("@fieldnotes/core");
238
164
  function useActiveTool() {
239
165
  const viewport = useViewport();
240
- const subscribe = (0, import_react5.useCallback)(
241
- (onStoreChange) => viewport.toolManager.onChange(onStoreChange),
242
- [viewport]
243
- );
244
- const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
245
- const toolName = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot);
166
+ const source = (0, import_react5.useMemo)(() => (0, import_core3.createActiveToolSource)(viewport), [viewport]);
167
+ const toolName = (0, import_react5.useSyncExternalStore)(source.subscribe, source.getSnapshot);
246
168
  const setTool = (0, import_react5.useCallback)((name) => viewport.setTool(name), [viewport]);
247
169
  return [toolName, setTool];
248
170
  }
249
171
 
250
172
  // src/hooks/use-camera.ts
251
173
  var import_react6 = require("react");
174
+ var import_core4 = require("@fieldnotes/core");
252
175
  function useCamera() {
253
176
  const viewport = useViewport();
254
- const cachedRef = (0, import_react6.useRef)({ x: 0, y: 0, zoom: 1 });
255
- const subscribe = (0, import_react6.useCallback)(
256
- (onStoreChange) => viewport.camera.onChange(onStoreChange),
257
- [viewport]
258
- );
259
- const getSnapshot = (0, import_react6.useCallback)(() => {
260
- const { position, zoom } = viewport.camera;
261
- const cached = cachedRef.current;
262
- if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
263
- return cached;
264
- }
265
- const next = { x: position.x, y: position.y, zoom };
266
- cachedRef.current = next;
267
- return next;
268
- }, [viewport]);
269
- return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot);
177
+ const source = (0, import_react6.useMemo)(() => (0, import_core4.createCameraSource)(viewport), [viewport]);
178
+ return (0, import_react6.useSyncExternalStore)(source.subscribe, source.getSnapshot);
270
179
  }
271
180
 
272
181
  // src/hooks/use-tool-options.ts
273
182
  var import_react7 = require("react");
274
- function noop() {
275
- }
183
+ var import_core5 = require("@fieldnotes/core");
276
184
  function useToolOptions(toolName) {
277
185
  const viewport = useViewport();
278
- const cachedRef = (0, import_react7.useRef)(null);
186
+ const nameRef = (0, import_react7.useRef)(toolName);
187
+ nameRef.current = toolName;
188
+ const source = (0, import_react7.useMemo)(
189
+ () => (0, import_core5.createToolOptionsSource)(viewport, () => nameRef.current),
190
+ [viewport]
191
+ );
279
192
  const subscribe = (0, import_react7.useCallback)(
280
- (onStoreChange) => {
281
- const tool = viewport.toolManager.getTool(toolName);
282
- if (!tool?.onOptionsChange) return noop;
283
- return tool.onOptionsChange(onStoreChange);
284
- },
285
- [viewport, toolName]
193
+ (onStoreChange) => source.subscribe(onStoreChange),
194
+ [source, toolName]
286
195
  );
287
- const getSnapshot = (0, import_react7.useCallback)(() => {
288
- const tool = viewport.toolManager.getTool(toolName);
289
- if (!tool?.getOptions) return null;
290
- const next = tool.getOptions();
291
- const cached = cachedRef.current;
292
- if (cached && shallowEqual(cached, next)) return cached;
293
- cachedRef.current = next;
294
- return next;
295
- }, [viewport, toolName]);
296
- const options = (0, import_react7.useSyncExternalStore)(subscribe, getSnapshot);
196
+ const options = (0, import_react7.useSyncExternalStore)(subscribe, source.getSnapshot);
297
197
  const setOptions = (0, import_react7.useCallback)(
298
198
  (partial) => {
299
199
  const tool = viewport.toolManager.getTool(toolName);
@@ -303,51 +203,14 @@ function useToolOptions(toolName) {
303
203
  );
304
204
  return [options, setOptions];
305
205
  }
306
- function shallowEqual(a, b) {
307
- const keysA = Object.keys(a);
308
- const keysB = Object.keys(b);
309
- if (keysA.length !== keysB.length) return false;
310
- for (const key of keysA) {
311
- const valA = a[key];
312
- const valB = b[key];
313
- if (valA === valB) continue;
314
- if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
315
- const objA = valA;
316
- const objB = valB;
317
- const innerKeys = Object.keys(objA);
318
- if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
319
- continue;
320
- }
321
- }
322
- return false;
323
- }
324
- return true;
325
- }
326
206
 
327
207
  // src/hooks/use-layers.ts
328
208
  var import_react8 = require("react");
209
+ var import_core6 = require("@fieldnotes/core");
329
210
  function useLayers() {
330
211
  const viewport = useViewport();
331
- const cachedRef = (0, import_react8.useRef)({ layers: [], activeLayerId: "" });
332
- const subscribe = (0, import_react8.useCallback)(
333
- (onStoreChange) => viewport.layerManager.on("change", onStoreChange),
334
- [viewport]
335
- );
336
- const getSnapshot = (0, import_react8.useCallback)(() => {
337
- const layers = viewport.layerManager.getLayers();
338
- const activeLayerId = viewport.layerManager.activeLayerId;
339
- const cached = cachedRef.current;
340
- if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
341
- const next2 = layers[i];
342
- 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;
343
- })) {
344
- return cached;
345
- }
346
- const next = { layers, activeLayerId };
347
- cachedRef.current = next;
348
- return next;
349
- }, [viewport]);
350
- const snapshot = (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot);
212
+ const source = (0, import_react8.useMemo)(() => (0, import_core6.createLayersSource)(viewport), [viewport]);
213
+ const snapshot = (0, import_react8.useSyncExternalStore)(source.subscribe, source.getSnapshot);
351
214
  const createLayer = (0, import_react8.useCallback)(
352
215
  (name) => viewport.layerManager.createLayer(name),
353
216
  [viewport]
@@ -401,25 +264,11 @@ function useLayers() {
401
264
 
402
265
  // src/hooks/use-history.ts
403
266
  var import_react9 = require("react");
267
+ var import_core7 = require("@fieldnotes/core");
404
268
  function useHistory() {
405
269
  const viewport = useViewport();
406
- const cachedRef = (0, import_react9.useRef)({ canUndo: false, canRedo: false });
407
- const subscribe = (0, import_react9.useCallback)(
408
- (onStoreChange) => viewport.history.onChange(onStoreChange),
409
- [viewport]
410
- );
411
- const getSnapshot = (0, import_react9.useCallback)(() => {
412
- const canUndo = viewport.history.canUndo;
413
- const canRedo = viewport.history.canRedo;
414
- const cached = cachedRef.current;
415
- if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
416
- return cached;
417
- }
418
- const next = { canUndo, canRedo };
419
- cachedRef.current = next;
420
- return next;
421
- }, [viewport]);
422
- const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot);
270
+ const source = (0, import_react9.useMemo)(() => (0, import_core7.createHistorySource)(viewport), [viewport]);
271
+ const snapshot = (0, import_react9.useSyncExternalStore)(source.subscribe, source.getSnapshot);
423
272
  const undo = (0, import_react9.useCallback)(() => viewport.undo(), [viewport]);
424
273
  const redo = (0, import_react9.useCallback)(() => viewport.redo(), [viewport]);
425
274
  return {
@@ -432,186 +281,69 @@ function useHistory() {
432
281
 
433
282
  // src/hooks/use-elements.ts
434
283
  var import_react10 = require("react");
435
- function shallowEqual2(a, b) {
436
- if (Object.is(a, b)) return true;
437
- if (Array.isArray(a) && Array.isArray(b)) {
438
- return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
439
- }
440
- if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
441
- const ka = Object.keys(a);
442
- const kb = Object.keys(b);
443
- return ka.length === kb.length && ka.every(
444
- (k) => k in b && Object.is(a[k], b[k])
445
- );
446
- }
447
- return false;
448
- }
284
+ var import_core8 = require("@fieldnotes/core");
449
285
  function useElements(arg, isEqual) {
450
286
  const viewport = useViewport();
451
- const cachedRef = (0, import_react10.useRef)([]);
452
- const selectedRef = (0, import_react10.useRef)(null);
453
- const subscribe = (0, import_react10.useCallback)(
454
- (onStoreChange) => viewport.store.onChange(onStoreChange),
287
+ const latest = (0, import_react10.useRef)({ arg, isEqual });
288
+ latest.current = { arg, isEqual };
289
+ const elementsSource = (0, import_react10.useMemo)(
290
+ () => (0, import_core8.createElementsSource)(viewport, () => {
291
+ const a = latest.current.arg;
292
+ return typeof a === "function" ? void 0 : a;
293
+ }),
455
294
  [viewport]
456
295
  );
457
- const getSnapshot = (0, import_react10.useCallback)(() => {
458
- if (typeof arg === "function") {
459
- const next2 = arg(viewport.store.getAll());
460
- const prev = selectedRef.current;
461
- const equal = isEqual ?? shallowEqual2;
462
- if (prev !== null && equal(prev.value, next2)) {
463
- return prev.value;
464
- }
465
- selectedRef.current = { value: next2 };
466
- return next2;
467
- }
468
- const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
469
- const cached = cachedRef.current;
470
- if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
471
- return cached;
472
- }
473
- cachedRef.current = next;
474
- return next;
475
- }, [viewport, arg, isEqual]);
476
- return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
296
+ const selectorSource = (0, import_react10.useMemo)(
297
+ () => (0, import_core8.createElementsSelectorSource)(
298
+ viewport,
299
+ // Only read while `arg` is a selector: the source is picked below by `typeof arg`.
300
+ () => latest.current.arg,
301
+ () => latest.current.isEqual
302
+ ),
303
+ [viewport]
304
+ );
305
+ const source = typeof arg === "function" ? selectorSource : elementsSource;
306
+ return (0, import_react10.useSyncExternalStore)(source.subscribe, source.getSnapshot);
477
307
  }
478
308
 
479
309
  // src/hooks/use-selection.ts
480
310
  var import_react11 = require("react");
311
+ var import_core9 = require("@fieldnotes/core");
481
312
  function useSelection() {
482
313
  const viewport = useViewport();
483
- const subscribe = (0, import_react11.useCallback)(
484
- (onChange) => viewport.onSelectionChange(onChange),
485
- [viewport]
486
- );
487
- const getSnapshot = (0, import_react11.useCallback)(() => viewport.getSelectedIds(), [viewport]);
488
- return (0, import_react11.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
314
+ const source = (0, import_react11.useMemo)(() => (0, import_core9.createSelectionSource)(viewport), [viewport]);
315
+ return (0, import_react11.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
489
316
  }
490
317
 
491
318
  // src/hooks/use-selection-style.ts
492
319
  var import_react12 = require("react");
493
-
494
- // src/hooks/style-keys.ts
495
- var STYLE_KEYS = [
496
- "color",
497
- "fillColor",
498
- "strokeWidth",
499
- "opacity",
500
- "fontSize",
501
- "strokeStyle"
502
- ];
503
-
504
- // src/hooks/use-selection-style.ts
505
- function styleEqual(a, b) {
506
- if (a === b) return true;
507
- if (a === null || b === null) return false;
508
- return STYLE_KEYS.every((k) => a[k] === b[k]);
509
- }
320
+ var import_core10 = require("@fieldnotes/core");
510
321
  function useSelectionStyle() {
511
322
  const viewport = useViewport();
512
- const cacheRef = (0, import_react12.useRef)(null);
513
- const subscribe = (0, import_react12.useCallback)(
514
- (onChange) => {
515
- const offSel = viewport.onSelectionChange(onChange);
516
- const offStore = viewport.store.onChange(onChange);
517
- return () => {
518
- offSel();
519
- offStore();
520
- };
521
- },
522
- [viewport]
523
- );
524
- const getSnapshot = (0, import_react12.useCallback)(() => {
525
- const next = viewport.getSelectionStyle();
526
- if (styleEqual(cacheRef.current, next)) return cacheRef.current;
527
- cacheRef.current = next;
528
- return next;
529
- }, [viewport]);
530
- const style = (0, import_react12.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
323
+ const source = (0, import_react12.useMemo)(() => (0, import_core10.createSelectionStyleSource)(viewport), [viewport]);
324
+ const style = (0, import_react12.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
531
325
  const apply = (0, import_react12.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
532
326
  return [style, apply];
533
327
  }
534
328
 
535
329
  // src/hooks/use-selection-style-details.ts
536
330
  var import_react13 = require("react");
537
- function keysEqual(a, b) {
538
- return a.length === b.length && a.every((k, i) => k === b[i]);
539
- }
540
- function detailsEqual(a, b) {
541
- if (a === b) return true;
542
- if (a === null || b === null) return false;
543
- return keysEqual(a.applicable, b.applicable) && keysEqual(a.mixed, b.mixed) && STYLE_KEYS.every((k) => a.common[k] === b.common[k]);
544
- }
331
+ var import_core11 = require("@fieldnotes/core");
545
332
  function useSelectionStyleDetails() {
546
333
  const viewport = useViewport();
547
- const cacheRef = (0, import_react13.useRef)(null);
548
- const subscribe = (0, import_react13.useCallback)(
549
- (onChange) => {
550
- const offSel = viewport.onSelectionChange(onChange);
551
- const offStore = viewport.store.onChange(onChange);
552
- return () => {
553
- offSel();
554
- offStore();
555
- };
556
- },
557
- [viewport]
558
- );
559
- const getSnapshot = (0, import_react13.useCallback)(() => {
560
- const next = viewport.getSelectionStyleDetails();
561
- if (detailsEqual(cacheRef.current, next)) return cacheRef.current;
562
- cacheRef.current = next;
563
- return next;
564
- }, [viewport]);
565
- const details = (0, import_react13.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
334
+ const source = (0, import_react13.useMemo)(() => (0, import_core11.createSelectionStyleDetailsSource)(viewport), [viewport]);
335
+ const details = (0, import_react13.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
566
336
  const apply = (0, import_react13.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
567
337
  return [details, apply];
568
338
  }
569
339
 
570
340
  // src/hooks/use-selection-ops.ts
571
341
  var import_react14 = require("react");
572
- function snapshotEqual(a, b) {
573
- 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;
574
- }
342
+ var import_core12 = require("@fieldnotes/core");
575
343
  function useSelectionOps() {
576
344
  const viewport = useViewport();
577
- const cacheRef = (0, import_react14.useRef)(null);
578
- const subscribe = (0, import_react14.useCallback)(
579
- (onChange) => {
580
- const offSel = viewport.onSelectionChange(onChange);
581
- const offStore = viewport.store.onChange(onChange);
582
- return () => {
583
- offSel();
584
- offStore();
585
- };
586
- },
587
- [viewport]
588
- );
589
- const getSnapshot = (0, import_react14.useCallback)(() => {
590
- const ids = viewport.getSelectedIds();
591
- const n = ids.length;
592
- let canUngroup = false;
593
- let lockedCount = 0;
594
- for (const id of ids) {
595
- const el = viewport.store.getById(id);
596
- if (!el) continue;
597
- if (el.groupId !== void 0) canUngroup = true;
598
- if (el.locked) lockedCount++;
599
- }
600
- const isLocked = n === 0 ? null : lockedCount === n ? true : lockedCount === 0 ? false : null;
601
- const next = {
602
- selectedIds: ids,
603
- canGroup: n >= 2,
604
- canUngroup,
605
- canAlign: n >= 2,
606
- canDistribute: n >= 3,
607
- isLocked
608
- };
609
- const prev = cacheRef.current;
610
- if (prev && snapshotEqual(prev, next)) return prev;
611
- cacheRef.current = next;
612
- return next;
613
- }, [viewport]);
614
- const snap = (0, import_react14.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
345
+ const source = (0, import_react14.useMemo)(() => (0, import_core12.createSelectionOpsSource)(viewport), [viewport]);
346
+ const snap = (0, import_react14.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
615
347
  const group = (0, import_react14.useCallback)(() => viewport.groupSelection(), [viewport]);
616
348
  const ungroup = (0, import_react14.useCallback)(() => viewport.ungroupSelection(), [viewport]);
617
349
  const toggleLock = (0, import_react14.useCallback)(() => viewport.toggleLockSelection(), [viewport]);
@@ -642,7 +374,7 @@ function useSelectionOps() {
642
374
 
643
375
  // src/hooks/use-element-rects.ts
644
376
  var import_react15 = require("react");
645
- var import_core2 = require("@fieldnotes/core");
377
+ var import_core13 = require("@fieldnotes/core");
646
378
  function useElementRects(match) {
647
379
  const viewport = useViewport();
648
380
  const matchRef = (0, import_react15.useRef)(match);
@@ -652,11 +384,11 @@ function useElementRects(match) {
652
384
  const trackerRef = (0, import_react15.useRef)(null);
653
385
  const subscribe = (0, import_react15.useCallback)(
654
386
  (onStoreChange) => {
655
- const tracker = new import_core2.ElementRectTracker(viewport, { match: stableMatch });
387
+ const tracker = new import_core13.ElementRectTracker(viewport, { match: stableMatch });
656
388
  trackerRef.current = tracker;
657
389
  const next = tracker.getRects();
658
390
  const previous = cacheRef.current;
659
- if (previous === null || !(0, import_core2.elementRectsEqual)(previous, next)) {
391
+ if (previous === null || !(0, import_core13.elementRectsEqual)(previous, next)) {
660
392
  cacheRef.current = next;
661
393
  onStoreChange();
662
394
  }
@@ -674,7 +406,7 @@ function useElementRects(match) {
674
406
  );
675
407
  const getSnapshot = (0, import_react15.useCallback)(() => {
676
408
  if (cacheRef.current === null) {
677
- cacheRef.current = (0, import_core2.computeElementRects)(viewport.store, stableMatch);
409
+ cacheRef.current = (0, import_core13.computeElementRects)(viewport.store, stableMatch);
678
410
  }
679
411
  return cacheRef.current;
680
412
  }, [viewport, stableMatch]);
@@ -687,7 +419,7 @@ function useElementRects(match) {
687
419
 
688
420
  // src/minimap.tsx
689
421
  var import_react16 = require("react");
690
- var import_core3 = require("@fieldnotes/core");
422
+ var import_core14 = require("@fieldnotes/core");
691
423
  var import_jsx_runtime2 = require("react/jsx-runtime");
692
424
  var toggleStyle = {
693
425
  position: "absolute",
@@ -719,7 +451,7 @@ function Minimap({
719
451
  if (collapsed) return;
720
452
  const canvas = canvasRef.current;
721
453
  if (!canvas) return;
722
- const controller = new import_core3.MinimapController(viewport, canvas, {
454
+ const controller = new import_core14.MinimapController(viewport, canvas, {
723
455
  width,
724
456
  height,
725
457
  background,