@fieldnotes/react 0.4.2 → 0.5.0

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
@@ -12,26 +12,88 @@ Requires React 18+.
12
12
 
13
13
  ## Quick Start
14
14
 
15
+ Full runnable example: `examples/react-app` — `pnpm --filter fieldnotes-react-example dev`
16
+
17
+ The canvas needs a container with a defined size — it fills whatever it's given:
18
+
15
19
  ```tsx
20
+ import { useState } from 'react';
16
21
  import { FieldNotesCanvas } from '@fieldnotes/react';
17
- import { HandTool, SelectTool, PencilTool } from '@fieldnotes/core';
22
+ import {
23
+ HandTool,
24
+ SelectTool,
25
+ PencilTool,
26
+ EraserTool,
27
+ NoteTool,
28
+ ShapeTool,
29
+ } from '@fieldnotes/core';
30
+
31
+ const TOOLS = [
32
+ new HandTool(),
33
+ new SelectTool(),
34
+ new PencilTool(),
35
+ new EraserTool(),
36
+ new NoteTool(),
37
+ new ShapeTool(),
38
+ ];
39
+
40
+ export function App() {
41
+ const [tool, setTool] = useState('select');
18
42
 
19
- function App() {
20
43
  return (
21
- <FieldNotesCanvas
22
- tools={[new HandTool(), new SelectTool(), new PencilTool()]}
23
- defaultTool="select"
24
- style={{ width: '100vw', height: '100vh' }}
25
- />
44
+ <div style={{ width: '100vw', height: '100vh' }}>
45
+ <FieldNotesCanvas
46
+ tools={TOOLS}
47
+ tool={tool}
48
+ onToolChange={setTool}
49
+ style={{ width: '100%', height: '100%' }}
50
+ >
51
+ <Toolbar tool={tool} onSelect={setTool} />
52
+ </FieldNotesCanvas>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ function Toolbar({ tool, onSelect }: { tool: string; onSelect: (name: string) => void }) {
58
+ const TOOL_LABELS: readonly (readonly [string, string])[] = [
59
+ ['select', 'Select'],
60
+ ['hand', 'Hand'],
61
+ ['pencil', 'Pencil'],
62
+ ['eraser', 'Eraser'],
63
+ ['note', 'Note'],
64
+ ['shape', 'Shape'],
65
+ ];
66
+
67
+ return (
68
+ <div className="toolbar">
69
+ {TOOL_LABELS.map(([name, label]) => (
70
+ <button key={name} className={tool === name ? 'active' : ''} onClick={() => onSelect(name)}>
71
+ {label}
72
+ </button>
73
+ ))}
74
+ </div>
26
75
  );
27
76
  }
28
77
  ```
29
78
 
30
- Your container needs a defined size — the canvas fills it.
79
+ ## Prop Reactivity
80
+
81
+ Not all props are reactive — the canvas is stateful. Changing a mount-only prop after mount has no effect:
82
+
83
+ | Prop | Reactivity |
84
+ | ---------------------------------- | ----------------------------------------------------------------------------------- |
85
+ | `options` | Mount-only — the canvas is stateful; construct with the right options |
86
+ | `tools` | Reactive, append-only (tools cannot be unregistered). Hoist the array out of render |
87
+ | `tool` / `onToolChange` | Reactive (controlled). Memoize the callback |
88
+ | `defaultTool` | Mount-only (uncontrolled initial tool) |
89
+ | `snapToGrid` | Reactive |
90
+ | `className` / `style` / `children` | Reactive (plain React) |
91
+
92
+ Runtime changes beyond these go through the viewport — access it via `useViewport()` or the ref: `viewport.setSnapToGrid(...)`, `viewport.shortcuts.rebind(...)`, `viewport.fitToContent()`. The background pattern is constructor-only (`options.background`).
31
93
 
32
94
  ## Embedding React Components
33
95
 
34
- The main feature — render any React component as a canvas element that pans, zooms, and resizes with the canvas:
96
+ The headline feature — render any React subtree as a canvas element that pans, zooms, and resizes with the canvas:
35
97
 
36
98
  ```tsx
37
99
  import { FieldNotesCanvas, CanvasElement } from '@fieldnotes/react';
@@ -58,222 +120,225 @@ function App() {
58
120
 
59
121
  Embedded components use a **two-mode interaction model**: by default they can be selected, dragged, and resized. **Double-click** to enter interact mode (clicks, inputs, forms work). **Escape** or click outside to exit.
60
122
 
61
- ## Hooks
62
-
63
- All hooks must be used inside `<FieldNotesCanvas>`.
123
+ `position` is required; `size` is optional — omit it to let the element size to its content. Both props are reactive: updating them moves or resizes the element on the canvas.
64
124
 
65
- ### `useActiveTool()`
125
+ ## Undo / Redo
66
126
 
67
- Reactive tool name + setter re-renders when the active tool changes:
127
+ `useHistory` returns reactive undo/redo state and action callbacks:
68
128
 
69
129
  ```tsx
70
- import { useActiveTool } from '@fieldnotes/react';
130
+ import { useHistory } from '@fieldnotes/react';
71
131
 
72
- function Toolbar() {
73
- const [tool, setTool] = useActiveTool();
132
+ function UndoRedo() {
133
+ const { canUndo, canRedo, undo, redo } = useHistory();
74
134
 
75
135
  return (
76
136
  <div>
77
- <span>Current: {tool}</span>
78
- <button onClick={() => setTool('pencil')}>Pencil</button>
79
- <button onClick={() => setTool('select')}>Select</button>
137
+ <button onClick={undo} disabled={!canUndo}>
138
+ Undo
139
+ </button>
140
+ <button onClick={redo} disabled={!canRedo}>
141
+ Redo
142
+ </button>
80
143
  </div>
81
144
  );
82
145
  }
83
146
  ```
84
147
 
85
- ### `useToolOptions(toolName)`
148
+ ## Why Is My Sidebar Re-rendering 60×/s?
86
149
 
87
- Reactive tool options with two-way syncread and write tool configuration:
150
+ `useElements()` with no arguments re-renders on every store mutation. For derived values, pass a selector the hook only re-renders when the selected value changes:
88
151
 
89
152
  ```tsx
90
- import { useActiveTool, useToolOptions } from '@fieldnotes/react';
91
- import type { PencilToolOptions } from '@fieldnotes/core';
153
+ import { useCallback } from 'react';
154
+ import { useElements } from '@fieldnotes/react';
155
+ import type { CanvasElement } from '@fieldnotes/core';
92
156
 
93
- function PencilSettings() {
94
- const [tool, setTool] = useActiveTool();
95
- const [opts, setOpts] = useToolOptions<PencilToolOptions>('pencil');
157
+ export function Sidebar() {
158
+ // Stable selector: useCallback with [] dependency or module-scope function
159
+ const count = useElements(useCallback((els: CanvasElement[]) => els.length, []));
160
+ const notes = useElements('note');
96
161
 
97
162
  return (
98
- <div>
99
- <button onClick={() => setTool('pencil')}>Pencil</button>
100
- {tool === 'pencil' && opts && (
101
- <>
102
- <input
103
- type="color"
104
- value={opts.color}
105
- onChange={(e) => setOpts({ color: e.target.value })}
106
- />
107
- <input
108
- type="range"
109
- min={1}
110
- max={20}
111
- value={opts.width}
112
- onChange={(e) => setOpts({ width: Number(e.target.value) })}
113
- />
114
- </>
115
- )}
163
+ <div className="sidebar">
164
+ <p>{count} elements</p>
165
+ <ul>
166
+ {notes.map((n) => (
167
+ <li key={n.id}>{n.text ? n.text.replace(/<[^>]+>/g, '').slice(0, 30) : '(empty)'}</li>
168
+ ))}
169
+ </ul>
116
170
  </div>
117
171
  );
118
172
  }
119
173
  ```
120
174
 
121
- Returns `[null, noop]` for tools that don't support options (e.g., `HandTool`).
175
+ Alternatively, hoist expensive rendering into a memoized child and pass elements as props — React's `memo` then does the comparison.
122
176
 
123
- ### `useLayers()`
177
+ **Default comparator is one-level shallow**: arrays are compared by index, plain objects by key, primitives by `Object.is`. Selectors returning nested fresh objects (e.g. `els => els.map(e => ({ ...e.position }))`) still cause re-renders with the default comparator — pass a custom `isEqual` as the second argument:
124
178
 
125
- Full layer management — reactive layer list with action callbacks:
179
+ ```tsx
180
+ const positions = useElements(
181
+ useCallback((els: CanvasElement[]) => els.map((e) => e.position), []),
182
+ (a, b) => a.length === b.length && a.every((p, i) => p.x === b[i]?.x && p.y === b[i]?.y),
183
+ );
184
+ ```
185
+
186
+ ## Save / Load
187
+
188
+ Access the viewport imperatively for export and import:
126
189
 
127
190
  ```tsx
128
- import { useLayers } from '@fieldnotes/react';
129
-
130
- function LayersPanel() {
131
- const {
132
- layers,
133
- activeLayerId,
134
- createLayer,
135
- removeLayer,
136
- setVisible,
137
- setLocked,
138
- setOpacity,
139
- setActiveLayer,
140
- } = useLayers();
191
+ import { useViewport } from '@fieldnotes/react';
192
+
193
+ export function SaveControls() {
194
+ const viewport = useViewport();
195
+
196
+ const save = () => {
197
+ localStorage.setItem('fieldnotes-example', viewport.exportJSON());
198
+ };
199
+ const load = () => {
200
+ const json = localStorage.getItem('fieldnotes-example');
201
+ if (json) viewport.loadJSON(json);
202
+ };
141
203
 
142
204
  return (
143
205
  <div>
144
- <button onClick={() => createLayer()}>Add Layer</button>
145
- {layers.map((layer) => (
146
- <div key={layer.id} onClick={() => setActiveLayer(layer.id)}>
147
- <span>
148
- {layer.name} {layer.id === activeLayerId ? '(active)' : ''}
149
- </span>
150
- <button onClick={() => setVisible(layer.id, !layer.visible)}>
151
- {layer.visible ? 'Hide' : 'Show'}
152
- </button>
153
- <button onClick={() => setLocked(layer.id, !layer.locked)}>
154
- {layer.locked ? 'Unlock' : 'Lock'}
155
- </button>
156
- <input
157
- type="range"
158
- min={0}
159
- max={1}
160
- step={0.1}
161
- value={layer.opacity}
162
- onChange={(e) => setOpacity(layer.id, Number(e.target.value))}
163
- />
164
- </div>
165
- ))}
206
+ <button onClick={save}>Save</button>
207
+ <button onClick={load}>Load</button>
166
208
  </div>
167
209
  );
168
210
  }
169
211
  ```
170
212
 
171
- Also exposes: `renameLayer`, `reorderLayer`, `moveElement`.
213
+ For periodic auto-saving, use `AutoSave` from `@fieldnotes/core` — it debounces writes to `localStorage` and subscribes to the store and camera automatically:
172
214
 
173
- ### `useHistory()`
215
+ ```tsx
216
+ import { AutoSave } from '@fieldnotes/core';
217
+
218
+ // in onReady or a useEffect after useViewport():
219
+ const autoSave = new AutoSave(viewport.store, viewport.camera, {
220
+ key: 'my-board',
221
+ layerManager: viewport.layerManager,
222
+ });
223
+ autoSave.start();
224
+ // call autoSave.stop() on cleanup
225
+ ```
174
226
 
175
- Reactive undo/redo state:
227
+ Pass `layerManager` — without it, saved boards lose their layer structure.
176
228
 
177
- ```tsx
178
- import { useHistory } from '@fieldnotes/react';
229
+ ## Custom Tools
179
230
 
180
- function UndoRedo() {
181
- const { canUndo, canRedo, undo, redo } = useHistory();
231
+ Implement the `Tool` interface from `@fieldnotes/core` and register it via the `tools` prop. Hoist the array so instances are not recreated on every render:
182
232
 
183
- return (
184
- <div>
185
- <button onClick={undo} disabled={!canUndo}>
186
- Undo
187
- </button>
188
- <button onClick={redo} disabled={!canRedo}>
189
- Redo
190
- </button>
191
- </div>
192
- );
233
+ ```tsx
234
+ import type { Tool, ToolContext, PointerState } from '@fieldnotes/core';
235
+ import { createNote } from '@fieldnotes/core';
236
+
237
+ export class StampTool implements Tool {
238
+ readonly name = 'stamp';
239
+
240
+ onPointerDown(state: PointerState, ctx: ToolContext): void {
241
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
242
+ ctx.store.add(
243
+ createNote({
244
+ position: { x: world.x - 50, y: world.y - 25 },
245
+ size: { w: 100, h: 50 },
246
+ text: 'stamp',
247
+ layerId: ctx.activeLayerId ?? '',
248
+ }),
249
+ );
250
+ ctx.requestRender();
251
+ }
252
+
253
+ onPointerMove(_state: PointerState, _ctx: ToolContext): void {}
254
+ onPointerUp(_state: PointerState, _ctx: ToolContext): void {}
193
255
  }
194
256
  ```
195
257
 
196
- ### `useElements(type?)`
197
-
198
- Reactive element list — re-renders when elements are added, removed, or updated:
258
+ Register alongside the built-in tools:
199
259
 
200
260
  ```tsx
201
- import { useElements } from '@fieldnotes/react';
261
+ import { HandTool, SelectTool } from '@fieldnotes/core';
262
+ import { StampTool } from './StampTool';
202
263
 
203
- function ElementCount() {
204
- const elements = useElements();
205
- const notes = useElements('note');
264
+ // Hoisted — stable reference across renders
265
+ const TOOLS = [new HandTool(), new SelectTool(), new StampTool()];
206
266
 
267
+ function App() {
268
+ const [tool, setTool] = useState('select');
207
269
  return (
208
- <span>
209
- {notes.length} notes / {elements.length} total
210
- </span>
270
+ <FieldNotesCanvas tools={TOOLS} tool={tool} onToolChange={setTool} style={...}>
271
+ <Toolbar tool={tool} onSelect={setTool} />
272
+ </FieldNotesCanvas>
211
273
  );
212
274
  }
213
275
  ```
214
276
 
215
- Pass an element type (`'note'`, `'stroke'`, `'arrow'`, etc.) to filter.
277
+ ## Events
216
278
 
217
- ### `useCamera()`
279
+ ### `onReady`
218
280
 
219
- Reactive camera state (position + zoom) re-renders on pan/zoom:
281
+ Fires once after the `Viewport` is created and tools are registered. Use it for imperative setup that can't wait for a child hook:
220
282
 
221
283
  ```tsx
222
- import { useCamera } from '@fieldnotes/react';
223
-
224
- function CameraInfo() {
225
- const { x, y, zoom } = useCamera();
226
-
227
- return (
228
- <span>
229
- {zoom.toFixed(2)}x at ({x.toFixed(0)}, {y.toFixed(0)})
230
- </span>
231
- );
232
- }
284
+ <FieldNotesCanvas
285
+ tools={TOOLS}
286
+ onReady={(viewport) => {
287
+ const saved = localStorage.getItem('board');
288
+ if (saved) viewport.loadJSON(saved);
289
+ }}
290
+ style={{ width: '100%', height: '100%' }}
291
+ />
233
292
  ```
234
293
 
235
- ### `useViewport()`
294
+ ### `onToolChange`
236
295
 
237
- Access the core `Viewport` instance for imperative operations not covered by the hooks above:
296
+ Fires whenever the active tool changes — from the keyboard, the API, or the controlled `tool` prop. Pair with `useState` and memoize with `useCallback` when the function body is non-trivial:
238
297
 
239
298
  ```tsx
240
- import { useViewport } from '@fieldnotes/react';
299
+ const [tool, setTool] = useState('select');
241
300
 
242
- function ExportButton() {
243
- const viewport = useViewport();
244
-
245
- return <button onClick={() => viewport.exportImage()}>Export PNG</button>;
246
- }
301
+ <FieldNotesCanvas tool={tool} onToolChange={setTool} tools={TOOLS} style={...} />
247
302
  ```
248
303
 
249
- ## Component API
304
+ ### `options.onImageError`
250
305
 
251
- ### `<FieldNotesCanvas>`
306
+ Called when an image element fails to load. Receives `{ src: string; elementIds: string[] }` — the source URL and all element IDs that reference it:
252
307
 
253
- | Prop | Type | Description |
254
- | ------------- | ------------------------------ | --------------------------------------- |
255
- | `options` | `ViewportOptions` | Camera and background config |
256
- | `tools` | `Tool[]` | Tools to register on mount |
257
- | `defaultTool` | `string` | Tool to activate on mount |
258
- | `className` | `string` | CSS class for the container div |
259
- | `style` | `CSSProperties` | Inline styles for the container div |
260
- | `onReady` | `(viewport: Viewport) => void` | Called after Viewport is created |
261
- | `children` | `ReactNode` | Child components (have access to hooks) |
262
- | `ref` | `Ref<FieldNotesCanvasRef>` | Exposes `{ viewport }` |
308
+ ```tsx
309
+ <FieldNotesCanvas
310
+ tools={TOOLS}
311
+ options={{
312
+ onImageError: ({ src, elementIds }) => {
313
+ console.warn(`Image failed: ${src}`, elementIds);
314
+ },
315
+ }}
316
+ style={{ width: '100%', height: '100%' }}
317
+ />
318
+ ```
263
319
 
264
- ### `<CanvasElement>`
320
+ ### `options.onDrop`
265
321
 
266
- | Prop | Type | Default | Description |
267
- | ---------- | -------------------------- | -------------------- | ---------------------------------- |
268
- | `position` | `{ x: number; y: number }` | required | World-space position |
269
- | `size` | `{ w: number; h: number }` | `{ w: 200, h: 150 }` | Element size in world-space pixels |
270
- | `children` | `ReactNode` | required | React content to render on canvas |
322
+ Fires for **every** drop event on the canvas surface. Providing this callback replaces the built-in image-drop handling entirely — if you want images to work, handle them yourself. Receives the original `DragEvent` and the world-space drop position:
271
323
 
272
- Position and size updates are reactive — change the props and the element moves/resizes on the canvas.
324
+ ```tsx
325
+ <FieldNotesCanvas
326
+ tools={TOOLS}
327
+ options={{
328
+ onDrop: (event, worldPosition) => {
329
+ const url = event.dataTransfer?.getData('text/uri-list');
330
+ if (url) {
331
+ // add your own image element at worldPosition
332
+ }
333
+ },
334
+ }}
335
+ style={{ width: '100%', height: '100%' }}
336
+ />
337
+ ```
273
338
 
274
- ## Accessing the Viewport Directly
339
+ ## Escape Hatch
275
340
 
276
- For advanced use cases, use a ref:
341
+ `useViewport()` returns the raw `Viewport` instance for anything not covered by the hooks above. A `ref` on `<FieldNotesCanvas>` gives the same access outside of child components:
277
342
 
278
343
  ```tsx
279
344
  import { useRef } from 'react';
@@ -282,20 +347,17 @@ import { FieldNotesCanvas, type FieldNotesCanvasRef } from '@fieldnotes/react';
282
347
  function App() {
283
348
  const canvasRef = useRef<FieldNotesCanvasRef>(null);
284
349
 
285
- const exportState = () => {
286
- const json = canvasRef.current?.viewport?.exportJSON();
287
- console.log(json);
288
- };
289
-
290
350
  return (
291
351
  <>
292
352
  <FieldNotesCanvas ref={canvasRef} style={{ width: '100vw', height: '100vh' }} />
293
- <button onClick={exportState}>Export</button>
353
+ <button onClick={() => canvasRef.current?.viewport?.fitToContent()}>Fit to content</button>
294
354
  </>
295
355
  );
296
356
  }
297
357
  ```
298
358
 
359
+ See the [core README](../core/README.md) for the full `Viewport` API — `exportJSON`/`loadJSON`, `fitToContent`, `shortcuts.rebind`, `setSnapToGrid`, and more.
360
+
299
361
  ## Versioning
300
362
 
301
363
  `@fieldnotes/core` and `@fieldnotes/react` are versioned independently. The react
package/dist/index.cjs CHANGED
@@ -44,7 +44,18 @@ var ViewportContext = (0, import_react.createContext)(null);
44
44
  // src/field-notes-canvas.tsx
45
45
  var import_jsx_runtime = require("react/jsx-runtime");
46
46
  var FieldNotesCanvas = (0, import_react2.forwardRef)(
47
- function FieldNotesCanvas2({ options, tools, defaultTool, className, style, children, onReady }, ref) {
47
+ function FieldNotesCanvas2({
48
+ options,
49
+ tools,
50
+ defaultTool,
51
+ tool,
52
+ onToolChange,
53
+ snapToGrid,
54
+ className,
55
+ style,
56
+ children,
57
+ onReady
58
+ }, ref) {
48
59
  const containerRef = (0, import_react2.useRef)(null);
49
60
  const [viewport, setViewport] = (0, import_react2.useState)(null);
50
61
  (0, import_react2.useImperativeHandle)(ref, () => ({ viewport }), [viewport]);
@@ -53,12 +64,13 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
53
64
  if (!el) return;
54
65
  const vp = new import_core.Viewport(el, options);
55
66
  if (tools) {
56
- for (const tool of tools) {
57
- vp.toolManager.register(tool);
67
+ for (const t of tools) {
68
+ vp.toolManager.register(t);
58
69
  }
59
70
  }
60
- if (defaultTool) {
61
- vp.toolManager.setTool(defaultTool, vp.toolContext);
71
+ const initialTool = tool ?? defaultTool;
72
+ if (initialTool) {
73
+ vp.setTool(initialTool);
62
74
  }
63
75
  setViewport(vp);
64
76
  onReady?.(vp);
@@ -67,6 +79,28 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
67
79
  setViewport(null);
68
80
  };
69
81
  }, []);
82
+ (0, import_react2.useEffect)(() => {
83
+ if (!viewport || tool === void 0) return;
84
+ if (viewport.toolManager.activeTool?.name !== tool) {
85
+ viewport.setTool(tool);
86
+ }
87
+ }, [viewport, tool]);
88
+ (0, import_react2.useEffect)(() => {
89
+ if (!viewport || !onToolChange) return;
90
+ return viewport.toolManager.onChange(onToolChange);
91
+ }, [viewport, onToolChange]);
92
+ (0, import_react2.useEffect)(() => {
93
+ if (!viewport || snapToGrid === void 0) return;
94
+ viewport.setSnapToGrid(snapToGrid);
95
+ }, [viewport, snapToGrid]);
96
+ (0, import_react2.useEffect)(() => {
97
+ if (!viewport || !tools) return;
98
+ for (const t of tools) {
99
+ if (!viewport.toolManager.getTool(t.name)) {
100
+ viewport.toolManager.register(t);
101
+ }
102
+ }
103
+ }, [viewport, tools]);
70
104
  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 }) });
71
105
  }
72
106
  );
@@ -127,15 +161,12 @@ var import_react5 = require("react");
127
161
  function useActiveTool() {
128
162
  const viewport = useViewport();
129
163
  const subscribe = (0, import_react5.useCallback)(
130
- (onStoreChange) => viewport.toolManager.onChange(() => onStoreChange()),
164
+ (onStoreChange) => viewport.toolManager.onChange(onStoreChange),
131
165
  [viewport]
132
166
  );
133
167
  const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
134
168
  const toolName = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot);
135
- const setTool = (0, import_react5.useCallback)(
136
- (name) => viewport.toolManager.setTool(name, viewport.toolContext),
137
- [viewport]
138
- );
169
+ const setTool = (0, import_react5.useCallback)((name) => viewport.setTool(name), [viewport]);
139
170
  return [toolName, setTool];
140
171
  }
141
172
 
@@ -324,22 +355,47 @@ function useHistory() {
324
355
 
325
356
  // src/hooks/use-elements.ts
326
357
  var import_react10 = require("react");
327
- function useElements(type) {
358
+ function shallowEqual2(a, b) {
359
+ if (Object.is(a, b)) return true;
360
+ if (Array.isArray(a) && Array.isArray(b)) {
361
+ return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
362
+ }
363
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
364
+ const ka = Object.keys(a);
365
+ const kb = Object.keys(b);
366
+ return ka.length === kb.length && ka.every(
367
+ (k) => k in b && Object.is(a[k], b[k])
368
+ );
369
+ }
370
+ return false;
371
+ }
372
+ function useElements(arg, isEqual) {
328
373
  const viewport = useViewport();
329
374
  const cachedRef = (0, import_react10.useRef)([]);
375
+ const selectedRef = (0, import_react10.useRef)(null);
330
376
  const subscribe = (0, import_react10.useCallback)(
331
377
  (onStoreChange) => viewport.store.onChange(onStoreChange),
332
378
  [viewport]
333
379
  );
334
380
  const getSnapshot = (0, import_react10.useCallback)(() => {
335
- const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();
381
+ if (typeof arg === "function") {
382
+ const next2 = arg(viewport.store.getAll());
383
+ const prev = selectedRef.current;
384
+ const equal = isEqual ?? shallowEqual2;
385
+ if (prev !== null && equal(prev.value, next2)) {
386
+ return prev.value;
387
+ }
388
+ selectedRef.current = { value: next2 };
389
+ return next2;
390
+ }
391
+ const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
336
392
  const cached = cachedRef.current;
337
393
  if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
338
394
  return cached;
339
395
  }
340
396
  cachedRef.current = next;
341
397
  return next;
342
- }, [viewport, type]);
398
+ }, [viewport, arg, isEqual]);
343
399
  return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
344
400
  }
345
401
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/field-notes-canvas.tsx","../src/context.ts","../src/canvas-element.tsx","../src/hooks/use-viewport.ts","../src/hooks/use-active-tool.ts","../src/hooks/use-camera.ts","../src/hooks/use-tool-options.ts","../src/hooks/use-layers.ts","../src/hooks/use-history.ts","../src/hooks/use-elements.ts"],"sourcesContent":["export { FieldNotesCanvas } from './field-notes-canvas';\r\nexport type { FieldNotesCanvasProps, FieldNotesCanvasRef } from './field-notes-canvas';\r\nexport { CanvasElement } from './canvas-element';\r\nexport type { CanvasElementProps } from './canvas-element';\r\nexport {\r\n useViewport,\r\n useActiveTool,\r\n useCamera,\r\n useToolOptions,\r\n useLayers,\r\n useHistory,\r\n useElements,\r\n} from './hooks';\r\nexport type { CameraState, UseLayersResult, UseHistoryResult } from './hooks';\r\nexport { ViewportContext } from './context';\r\n","import {\r\n useRef,\r\n useEffect,\r\n useState,\r\n forwardRef,\r\n useImperativeHandle,\r\n type ReactNode,\r\n type CSSProperties,\r\n} from 'react';\r\nimport { Viewport } from '@fieldnotes/core';\r\nimport type { ViewportOptions, Tool } from '@fieldnotes/core';\r\nimport { ViewportContext } from './context';\r\n\r\nexport interface FieldNotesCanvasProps {\r\n options?: ViewportOptions;\r\n tools?: Tool[];\r\n defaultTool?: string;\r\n className?: string;\r\n style?: CSSProperties;\r\n children?: ReactNode;\r\n onReady?: (viewport: Viewport) => void;\r\n}\r\n\r\nexport interface FieldNotesCanvasRef {\r\n viewport: Viewport | null;\r\n}\r\n\r\nexport const FieldNotesCanvas = forwardRef<FieldNotesCanvasRef, FieldNotesCanvasProps>(\r\n function FieldNotesCanvas(\r\n { options, tools, defaultTool, className, style, children, onReady },\r\n ref,\r\n ) {\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const [viewport, setViewport] = useState<Viewport | null>(null);\r\n\r\n useImperativeHandle(ref, () => ({ viewport }), [viewport]);\r\n\r\n useEffect(() => {\r\n const el = containerRef.current;\r\n if (!el) return;\r\n\r\n const vp = new Viewport(el, options);\r\n\r\n if (tools) {\r\n for (const tool of tools) {\r\n vp.toolManager.register(tool);\r\n }\r\n }\r\n\r\n if (defaultTool) {\r\n vp.toolManager.setTool(defaultTool, vp.toolContext);\r\n }\r\n\r\n setViewport(vp);\r\n onReady?.(vp);\r\n\r\n return () => {\r\n vp.destroy();\r\n setViewport(null);\r\n };\r\n }, []);\r\n\r\n return (\r\n <div ref={containerRef} className={className} style={style}>\r\n {viewport && (\r\n <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\n","import { createContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\n\r\nexport const ViewportContext = createContext<Viewport | null>(null);\r\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\r\nimport { createPortal } from 'react-dom';\r\nimport { useViewport } from './hooks/use-viewport';\r\n\r\nexport interface CanvasElementProps {\r\n position: { x: number; y: number };\r\n size?: { w: number; h: number };\r\n children: ReactNode;\r\n}\r\n\r\nexport function CanvasElement({ position, size, children }: CanvasElementProps) {\r\n const viewport = useViewport();\r\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\r\n const elementIdRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n const container = document.createElement('div');\r\n Object.assign(container.style, {\r\n width: '100%',\r\n height: '100%',\r\n });\r\n\r\n // Append to domLayer immediately so portal children are queryable in the document\r\n // before the viewport render loop fires via requestAnimationFrame.\r\n viewport.domLayer.appendChild(container);\r\n\r\n const id = viewport.addHtmlElement(container, position, size);\r\n elementIdRef.current = id;\r\n setPortalTarget(container);\r\n\r\n return () => {\r\n if (elementIdRef.current) {\r\n viewport.store.remove(elementIdRef.current);\r\n viewport.requestRender();\r\n elementIdRef.current = null;\r\n }\r\n setPortalTarget(null);\r\n };\r\n }, [viewport]);\r\n\r\n useEffect(() => {\r\n const id = elementIdRef.current;\r\n if (!id) return;\r\n viewport.store.update(id, { position });\r\n if (size) {\r\n viewport.store.update(id, { size });\r\n }\r\n viewport.requestRender();\r\n }, [viewport, position.x, position.y, size?.w, size?.h]);\r\n\r\n if (!portalTarget) return null;\r\n return createPortal(children, portalTarget);\r\n}\r\n","import { useContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\nimport { ViewportContext } from '../context';\r\n\r\nexport function useViewport(): Viewport {\r\n const viewport = useContext(ViewportContext);\r\n if (!viewport) {\r\n throw new Error('useViewport must be used inside <FieldNotesCanvas>');\r\n }\r\n return viewport;\r\n}\r\n","import { useCallback, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport function useActiveTool(): [toolName: string, setTool: (name: string) => void] {\r\n const viewport = useViewport();\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.toolManager.onChange(() => onStoreChange()),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? '', [viewport]);\r\n\r\n const toolName = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const setTool = useCallback(\r\n (name: string) => viewport.toolManager.setTool(name, viewport.toolContext),\r\n [viewport],\r\n );\r\n\r\n return [toolName, setTool];\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport interface CameraState {\r\n x: number;\r\n y: number;\r\n zoom: number;\r\n}\r\n\r\nexport function useCamera(): CameraState {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<CameraState>({ x: 0, y: 0, zoom: 1 });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.camera.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): CameraState => {\r\n const { position, zoom } = viewport.camera;\r\n const cached = cachedRef.current;\r\n if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {\r\n return cached;\r\n }\r\n const next = { x: position.x, y: position.y, zoom };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nfunction noop() {\r\n /* unsubscribe placeholder */\r\n}\r\n\r\nexport function useToolOptions<T extends Record<string, unknown>>(\r\n toolName: string,\r\n): [options: T | null, setOptions: (partial: Partial<T>) => void] {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<T | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n if (!tool?.onOptionsChange) return noop;\r\n return tool.onOptionsChange(onStoreChange);\r\n },\r\n [viewport, toolName],\r\n );\r\n\r\n const getSnapshot = useCallback((): T | null => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n if (!tool?.getOptions) return null;\r\n const next = tool.getOptions() as T;\r\n const cached = cachedRef.current;\r\n if (cached && shallowEqual(cached, next)) return cached;\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport, toolName]);\r\n\r\n const options = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const setOptions = useCallback(\r\n (partial: Partial<T>) => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n tool?.setOptions?.(partial as Record<string, unknown>);\r\n },\r\n [viewport, toolName],\r\n );\r\n\r\n return [options, setOptions];\r\n}\r\n\r\nfunction shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\r\n const keysA = Object.keys(a);\r\n const keysB = Object.keys(b);\r\n if (keysA.length !== keysB.length) return false;\r\n for (const key of keysA) {\r\n const valA = a[key];\r\n const valB = b[key];\r\n if (valA === valB) continue;\r\n if (\r\n typeof valA === 'object' &&\r\n valA !== null &&\r\n typeof valB === 'object' &&\r\n valB !== null &&\r\n !Array.isArray(valA) &&\r\n !Array.isArray(valB)\r\n ) {\r\n const objA = valA as Record<string, unknown>;\r\n const objB = valB as Record<string, unknown>;\r\n const innerKeys = Object.keys(objA);\r\n if (\r\n innerKeys.length === Object.keys(objB).length &&\r\n innerKeys.every((k) => objA[k] === objB[k])\r\n ) {\r\n continue;\r\n }\r\n }\r\n return false;\r\n }\r\n return true;\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport type { Layer } from '@fieldnotes/core';\r\nimport { useViewport } from './use-viewport';\r\n\r\ninterface LayersSnapshot {\r\n layers: Layer[];\r\n activeLayerId: string;\r\n}\r\n\r\nexport interface UseLayersResult {\r\n layers: Layer[];\r\n activeLayerId: string;\r\n createLayer: (name?: string) => Layer;\r\n removeLayer: (id: string) => void;\r\n renameLayer: (id: string, name: string) => void;\r\n reorderLayer: (id: string, newOrder: number) => void;\r\n setVisible: (id: string, visible: boolean) => void;\r\n setLocked: (id: string, locked: boolean) => void;\r\n setOpacity: (id: string, opacity: number) => void;\r\n setActiveLayer: (id: string) => void;\r\n moveElement: (elementId: string, layerId: string) => void;\r\n}\r\n\r\nexport function useLayers(): UseLayersResult {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<LayersSnapshot>({ layers: [], activeLayerId: '' });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.layerManager.on('change', onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): LayersSnapshot => {\r\n const layers = viewport.layerManager.getLayers();\r\n const activeLayerId = viewport.layerManager.activeLayerId;\r\n const cached = cachedRef.current;\r\n if (\r\n cached.activeLayerId === activeLayerId &&\r\n cached.layers.length === layers.length &&\r\n cached.layers.every((l, i) => {\r\n const next = layers[i];\r\n return (\r\n next !== undefined &&\r\n l.id === next.id &&\r\n l.name === next.name &&\r\n l.visible === next.visible &&\r\n l.locked === next.locked &&\r\n l.order === next.order &&\r\n l.opacity === next.opacity\r\n );\r\n })\r\n ) {\r\n return cached;\r\n }\r\n const next = { layers, activeLayerId };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const createLayer = useCallback(\r\n (name?: string) => viewport.layerManager.createLayer(name),\r\n [viewport],\r\n );\r\n\r\n const removeLayer = useCallback(\r\n (id: string) => viewport.layerManager.removeLayer(id),\r\n [viewport],\r\n );\r\n\r\n const renameLayer = useCallback(\r\n (id: string, name: string) => viewport.layerManager.renameLayer(id, name),\r\n [viewport],\r\n );\r\n\r\n const reorderLayer = useCallback(\r\n (id: string, newOrder: number) => viewport.layerManager.reorderLayer(id, newOrder),\r\n [viewport],\r\n );\r\n\r\n const setVisible = useCallback(\r\n (id: string, visible: boolean) => viewport.layerManager.setLayerVisible(id, visible),\r\n [viewport],\r\n );\r\n\r\n const setLocked = useCallback(\r\n (id: string, locked: boolean) => viewport.layerManager.setLayerLocked(id, locked),\r\n [viewport],\r\n );\r\n\r\n const setOpacity = useCallback(\r\n (id: string, opacity: number) => viewport.layerManager.setLayerOpacity(id, opacity),\r\n [viewport],\r\n );\r\n\r\n const setActiveLayer = useCallback(\r\n (id: string) => viewport.layerManager.setActiveLayer(id),\r\n [viewport],\r\n );\r\n\r\n const moveElement = useCallback(\r\n (elementId: string, layerId: string) =>\r\n viewport.layerManager.moveElementToLayer(elementId, layerId),\r\n [viewport],\r\n );\r\n\r\n return {\r\n layers: snapshot.layers,\r\n activeLayerId: snapshot.activeLayerId,\r\n createLayer,\r\n removeLayer,\r\n renameLayer,\r\n reorderLayer,\r\n setVisible,\r\n setLocked,\r\n setOpacity,\r\n setActiveLayer,\r\n moveElement,\r\n };\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\ninterface HistorySnapshot {\r\n canUndo: boolean;\r\n canRedo: boolean;\r\n}\r\n\r\nexport interface UseHistoryResult {\r\n canUndo: boolean;\r\n canRedo: boolean;\r\n undo: () => void;\r\n redo: () => void;\r\n}\r\n\r\nexport function useHistory(): UseHistoryResult {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<HistorySnapshot>({ canUndo: false, canRedo: false });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.history.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): HistorySnapshot => {\r\n const canUndo = viewport.history.canUndo;\r\n const canRedo = viewport.history.canRedo;\r\n const cached = cachedRef.current;\r\n if (cached.canUndo === canUndo && cached.canRedo === canRedo) {\r\n return cached;\r\n }\r\n const next = { canUndo, canRedo };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const undo = useCallback(() => viewport.undo(), [viewport]);\r\n const redo = useCallback(() => viewport.redo(), [viewport]);\r\n\r\n return {\r\n canUndo: snapshot.canUndo,\r\n canRedo: snapshot.canRedo,\r\n undo,\r\n redo,\r\n };\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport type { CanvasElement, ElementType } from '@fieldnotes/core';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport function useElements(): CanvasElement[];\r\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\r\nexport function useElements(type?: ElementType): CanvasElement[] {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<CanvasElement[]>([]);\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): CanvasElement[] => {\r\n const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();\r\n const cached = cachedRef.current;\r\n if (cached.length === next.length && cached.every((el, i) => el === next[i])) {\r\n return cached;\r\n }\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport, type]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,gBAQO;AACP,kBAAyB;;;ACTzB,mBAA8B;AAGvB,IAAM,sBAAkB,4BAA+B,IAAI;;;AD8DxD;AAtCH,IAAM,uBAAmB;AAAA,EAC9B,SAASC,kBACP,EAAE,SAAS,OAAO,aAAa,WAAW,OAAO,UAAU,QAAQ,GACnE,KACA;AACA,UAAM,mBAAe,sBAAuB,IAAI;AAChD,UAAM,CAAC,UAAU,WAAW,QAAI,wBAA0B,IAAI;AAE9D,2CAAoB,KAAK,OAAO,EAAE,SAAS,IAAI,CAAC,QAAQ,CAAC;AAEzD,iCAAU,MAAM;AACd,YAAM,KAAK,aAAa;AACxB,UAAI,CAAC,GAAI;AAET,YAAM,KAAK,IAAI,qBAAS,IAAI,OAAO;AAEnC,UAAI,OAAO;AACT,mBAAW,QAAQ,OAAO;AACxB,aAAG,YAAY,SAAS,IAAI;AAAA,QAC9B;AAAA,MACF;AAEA,UAAI,aAAa;AACf,WAAG,YAAY,QAAQ,aAAa,GAAG,WAAW;AAAA,MACpD;AAEA,kBAAY,EAAE;AACd,gBAAU,EAAE;AAEZ,aAAO,MAAM;AACX,WAAG,QAAQ;AACX,oBAAY,IAAI;AAAA,MAClB;AAAA,IACF,GAAG,CAAC,CAAC;AAEL,WACE,4CAAC,SAAI,KAAK,cAAc,WAAsB,OAC3C,sBACC,4CAAC,gBAAgB,UAAhB,EAAyB,OAAO,UAAW,UAAS,GAEzD;AAAA,EAEJ;AACF;;;AEtEA,IAAAC,gBAA4D;AAC5D,uBAA6B;;;ACD7B,IAAAC,gBAA2B;AAIpB,SAAS,cAAwB;AACtC,QAAM,eAAW,0BAAW,eAAe;AAC3C,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE;AACA,SAAO;AACT;;;ADAO,SAAS,cAAc,EAAE,UAAU,MAAM,SAAS,GAAuB;AAC9E,QAAM,WAAW,YAAY;AAC7B,QAAM,CAAC,cAAc,eAAe,QAAI,wBAA6B,IAAI;AACzE,QAAM,mBAAe,sBAAsB,IAAI;AAE/C,+BAAU,MAAM;AACd,UAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,WAAO,OAAO,UAAU,OAAO;AAAA,MAC7B,OAAO;AAAA,MACP,QAAQ;AAAA,IACV,CAAC;AAID,aAAS,SAAS,YAAY,SAAS;AAEvC,UAAM,KAAK,SAAS,eAAe,WAAW,UAAU,IAAI;AAC5D,iBAAa,UAAU;AACvB,oBAAgB,SAAS;AAEzB,WAAO,MAAM;AACX,UAAI,aAAa,SAAS;AACxB,iBAAS,MAAM,OAAO,aAAa,OAAO;AAC1C,iBAAS,cAAc;AACvB,qBAAa,UAAU;AAAA,MACzB;AACA,sBAAgB,IAAI;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,+BAAU,MAAM;AACd,UAAM,KAAK,aAAa;AACxB,QAAI,CAAC,GAAI;AACT,aAAS,MAAM,OAAO,IAAI,EAAE,SAAS,CAAC;AACtC,QAAI,MAAM;AACR,eAAS,MAAM,OAAO,IAAI,EAAE,KAAK,CAAC;AAAA,IACpC;AACA,aAAS,cAAc;AAAA,EACzB,GAAG,CAAC,UAAU,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC;AAEvD,MAAI,CAAC,aAAc,QAAO;AAC1B,aAAO,+BAAa,UAAU,YAAY;AAC5C;;;AEpDA,IAAAC,gBAAkD;AAG3C,SAAS,gBAAqE;AACnF,QAAM,WAAW,YAAY;AAE7B,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B,SAAS,YAAY,SAAS,MAAM,cAAc,CAAC;AAAA,IAClF,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc,2BAAY,MAAM,SAAS,YAAY,YAAY,QAAQ,IAAI,CAAC,QAAQ,CAAC;AAE7F,QAAM,eAAW,oCAAqB,WAAW,WAAW;AAE5D,QAAM,cAAU;AAAA,IACd,CAAC,SAAiB,SAAS,YAAY,QAAQ,MAAM,SAAS,WAAW;AAAA,IACzE,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO,CAAC,UAAU,OAAO;AAC3B;;;ACrBA,IAAAC,gBAA0D;AASnD,SAAS,YAAyB;AACvC,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAY,sBAAoB,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,EAAE,CAAC;AAE7D,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B,SAAS,OAAO,SAAS,aAAa;AAAA,IACrE,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc,2BAAY,MAAmB;AACjD,UAAM,EAAE,UAAU,KAAK,IAAI,SAAS;AACpC,UAAM,SAAS,UAAU;AACzB,QAAI,OAAO,MAAM,SAAS,KAAK,OAAO,MAAM,SAAS,KAAK,OAAO,SAAS,MAAM;AAC9E,aAAO;AAAA,IACT;AACA,UAAM,OAAO,EAAE,GAAG,SAAS,GAAG,GAAG,SAAS,GAAG,KAAK;AAClD,cAAU,UAAU;AACpB,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,CAAC;AAEb,aAAO,oCAAqB,WAAW,WAAW;AACpD;;;AC9BA,IAAAC,gBAA0D;AAG1D,SAAS,OAAO;AAEhB;AAEO,SAAS,eACd,UACgE;AAChE,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAY,sBAAiB,IAAI;AAEvC,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B;AAC7B,YAAM,OAAO,SAAS,YAAY,QAAQ,QAAQ;AAClD,UAAI,CAAC,MAAM,gBAAiB,QAAO;AACnC,aAAO,KAAK,gBAAgB,aAAa;AAAA,IAC3C;AAAA,IACA,CAAC,UAAU,QAAQ;AAAA,EACrB;AAEA,QAAM,kBAAc,2BAAY,MAAgB;AAC9C,UAAM,OAAO,SAAS,YAAY,QAAQ,QAAQ;AAClD,QAAI,CAAC,MAAM,WAAY,QAAO;AAC9B,UAAM,OAAO,KAAK,WAAW;AAC7B,UAAM,SAAS,UAAU;AACzB,QAAI,UAAU,aAAa,QAAQ,IAAI,EAAG,QAAO;AACjD,cAAU,UAAU;AACpB,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,QAAQ,CAAC;AAEvB,QAAM,cAAU,oCAAqB,WAAW,WAAW;AAE3D,QAAM,iBAAa;AAAA,IACjB,CAAC,YAAwB;AACvB,YAAM,OAAO,SAAS,YAAY,QAAQ,QAAQ;AAClD,YAAM,aAAa,OAAkC;AAAA,IACvD;AAAA,IACA,CAAC,UAAU,QAAQ;AAAA,EACrB;AAEA,SAAO,CAAC,SAAS,UAAU;AAC7B;AAEA,SAAS,aAAa,GAA4B,GAAqC;AACrF,QAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,QAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,MAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAC1C,aAAW,OAAO,OAAO;AACvB,UAAM,OAAO,EAAE,GAAG;AAClB,UAAM,OAAO,EAAE,GAAG;AAClB,QAAI,SAAS,KAAM;AACnB,QACE,OAAO,SAAS,YAChB,SAAS,QACT,OAAO,SAAS,YAChB,SAAS,QACT,CAAC,MAAM,QAAQ,IAAI,KACnB,CAAC,MAAM,QAAQ,IAAI,GACnB;AACA,YAAM,OAAO;AACb,YAAM,OAAO;AACb,YAAM,YAAY,OAAO,KAAK,IAAI;AAClC,UACE,UAAU,WAAW,OAAO,KAAK,IAAI,EAAE,UACvC,UAAU,MAAM,CAAC,MAAM,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,GAC1C;AACA;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;AC1EA,IAAAC,gBAA0D;AAuBnD,SAAS,YAA6B;AAC3C,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAY,sBAAuB,EAAE,QAAQ,CAAC,GAAG,eAAe,GAAG,CAAC;AAE1E,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B,SAAS,aAAa,GAAG,UAAU,aAAa;AAAA,IAC/E,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc,2BAAY,MAAsB;AACpD,UAAM,SAAS,SAAS,aAAa,UAAU;AAC/C,UAAM,gBAAgB,SAAS,aAAa;AAC5C,UAAM,SAAS,UAAU;AACzB,QACE,OAAO,kBAAkB,iBACzB,OAAO,OAAO,WAAW,OAAO,UAChC,OAAO,OAAO,MAAM,CAAC,GAAG,MAAM;AAC5B,YAAMC,QAAO,OAAO,CAAC;AACrB,aACEA,UAAS,UACT,EAAE,OAAOA,MAAK,MACd,EAAE,SAASA,MAAK,QAChB,EAAE,YAAYA,MAAK,WACnB,EAAE,WAAWA,MAAK,UAClB,EAAE,UAAUA,MAAK,SACjB,EAAE,YAAYA,MAAK;AAAA,IAEvB,CAAC,GACD;AACA,aAAO;AAAA,IACT;AACA,UAAM,OAAO,EAAE,QAAQ,cAAc;AACrC,cAAU,UAAU;AACpB,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,eAAW,oCAAqB,WAAW,WAAW;AAE5D,QAAM,kBAAc;AAAA,IAClB,CAAC,SAAkB,SAAS,aAAa,YAAY,IAAI;AAAA,IACzD,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc;AAAA,IAClB,CAAC,OAAe,SAAS,aAAa,YAAY,EAAE;AAAA,IACpD,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc;AAAA,IAClB,CAAC,IAAY,SAAiB,SAAS,aAAa,YAAY,IAAI,IAAI;AAAA,IACxE,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,mBAAe;AAAA,IACnB,CAAC,IAAY,aAAqB,SAAS,aAAa,aAAa,IAAI,QAAQ;AAAA,IACjF,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,iBAAa;AAAA,IACjB,CAAC,IAAY,YAAqB,SAAS,aAAa,gBAAgB,IAAI,OAAO;AAAA,IACnF,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,gBAAY;AAAA,IAChB,CAAC,IAAY,WAAoB,SAAS,aAAa,eAAe,IAAI,MAAM;AAAA,IAChF,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,iBAAa;AAAA,IACjB,CAAC,IAAY,YAAoB,SAAS,aAAa,gBAAgB,IAAI,OAAO;AAAA,IAClF,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,qBAAiB;AAAA,IACrB,CAAC,OAAe,SAAS,aAAa,eAAe,EAAE;AAAA,IACvD,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc;AAAA,IAClB,CAAC,WAAmB,YAClB,SAAS,aAAa,mBAAmB,WAAW,OAAO;AAAA,IAC7D,CAAC,QAAQ;AAAA,EACX;AAEA,SAAO;AAAA,IACL,QAAQ,SAAS;AAAA,IACjB,eAAe,SAAS;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACxHA,IAAAC,gBAA0D;AAenD,SAAS,aAA+B;AAC7C,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAY,sBAAwB,EAAE,SAAS,OAAO,SAAS,MAAM,CAAC;AAE5E,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B,SAAS,QAAQ,SAAS,aAAa;AAAA,IACtE,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc,2BAAY,MAAuB;AACrD,UAAM,UAAU,SAAS,QAAQ;AACjC,UAAM,UAAU,SAAS,QAAQ;AACjC,UAAM,SAAS,UAAU;AACzB,QAAI,OAAO,YAAY,WAAW,OAAO,YAAY,SAAS;AAC5D,aAAO;AAAA,IACT;AACA,UAAM,OAAO,EAAE,SAAS,QAAQ;AAChC,cAAU,UAAU;AACpB,WAAO;AAAA,EACT,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,eAAW,oCAAqB,WAAW,WAAW;AAE5D,QAAM,WAAO,2BAAY,MAAM,SAAS,KAAK,GAAG,CAAC,QAAQ,CAAC;AAC1D,QAAM,WAAO,2BAAY,MAAM,SAAS,KAAK,GAAG,CAAC,QAAQ,CAAC;AAE1D,SAAO;AAAA,IACL,SAAS,SAAS;AAAA,IAClB,SAAS,SAAS;AAAA,IAClB;AAAA,IACA;AAAA,EACF;AACF;;;AC/CA,IAAAC,iBAA0D;AAMnD,SAAS,YAAY,MAAqC;AAC/D,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAY,uBAAwB,CAAC,CAAC;AAE5C,QAAM,gBAAY;AAAA,IAChB,CAAC,kBAA8B,SAAS,MAAM,SAAS,aAAa;AAAA,IACpE,CAAC,QAAQ;AAAA,EACX;AAEA,QAAM,kBAAc,4BAAY,MAAuB;AACrD,UAAM,OAAO,OAAO,SAAS,MAAM,kBAAkB,IAAI,IAAI,SAAS,MAAM,OAAO;AACnF,UAAM,SAAS,UAAU;AACzB,QAAI,OAAO,WAAW,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC,GAAG;AAC5E,aAAO;AAAA,IACT;AACA,cAAU,UAAU;AACpB,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,IAAI,CAAC;AAEnB,aAAO,qCAAqB,WAAW,WAAW;AACpD;","names":["import_react","FieldNotesCanvas","import_react","import_react","import_react","import_react","import_react","import_react","next","import_react","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/field-notes-canvas.tsx","../src/context.ts","../src/canvas-element.tsx","../src/hooks/use-viewport.ts","../src/hooks/use-active-tool.ts","../src/hooks/use-camera.ts","../src/hooks/use-tool-options.ts","../src/hooks/use-layers.ts","../src/hooks/use-history.ts","../src/hooks/use-elements.ts"],"sourcesContent":["export { FieldNotesCanvas } from './field-notes-canvas';\r\nexport type { FieldNotesCanvasProps, FieldNotesCanvasRef } from './field-notes-canvas';\r\nexport { CanvasElement } from './canvas-element';\r\nexport type { CanvasElementProps } from './canvas-element';\r\nexport {\r\n useViewport,\r\n useActiveTool,\r\n useCamera,\r\n useToolOptions,\r\n useLayers,\r\n useHistory,\r\n useElements,\r\n} from './hooks';\r\nexport type { CameraState, UseLayersResult, UseHistoryResult } from './hooks';\r\nexport { ViewportContext } from './context';\r\n","import {\n useRef,\n useEffect,\n useState,\n forwardRef,\n useImperativeHandle,\n type ReactNode,\n type CSSProperties,\n} from 'react';\nimport { Viewport } from '@fieldnotes/core';\nimport type { ViewportOptions, Tool } from '@fieldnotes/core';\nimport { ViewportContext } from './context';\n\nexport interface FieldNotesCanvasProps {\n /** Constructor-only: changes after mount are ignored (the canvas is stateful). */\n options?: ViewportOptions;\n /**\n * Reactive (append-only): tools present here are registered; removal is not supported.\n * Hoist the array out of render — inline arrays reconstruct Tool instances and re-run\n * registration on every render.\n */\n tools?: Tool[];\n /** Uncontrolled initial tool. Ignored when `tool` is set. */\n defaultTool?: string;\n /** Controlled active tool. Pair with `onToolChange`. */\n tool?: string;\n /**\n * Fires whenever the active tool changes (keyboard, API, or `tool` prop).\n * Memoize with useCallback to avoid re-subscribing each render.\n */\n onToolChange?: (name: string) => void;\n /** Reactive: toggles grid snapping. */\n snapToGrid?: boolean;\n className?: string;\n style?: CSSProperties;\n children?: ReactNode;\n onReady?: (viewport: Viewport) => void;\n}\n\nexport interface FieldNotesCanvasRef {\n viewport: Viewport | null;\n}\n\nexport const FieldNotesCanvas = forwardRef<FieldNotesCanvasRef, FieldNotesCanvasProps>(\n function FieldNotesCanvas(\n {\n options,\n tools,\n defaultTool,\n tool,\n onToolChange,\n snapToGrid,\n className,\n style,\n children,\n onReady,\n },\n ref,\n ) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [viewport, setViewport] = useState<Viewport | null>(null);\n\n useImperativeHandle(ref, () => ({ viewport }), [viewport]);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const vp = new Viewport(el, options);\n\n if (tools) {\n for (const t of tools) {\n vp.toolManager.register(t);\n }\n }\n\n const initialTool = tool ?? defaultTool;\n if (initialTool) {\n vp.setTool(initialTool);\n }\n\n setViewport(vp);\n onReady?.(vp);\n\n return () => {\n vp.destroy();\n setViewport(null);\n };\n // Constructor effect: options/tools/defaultTool are mount-only by design.\n }, []);\n\n useEffect(() => {\n if (!viewport || tool === undefined) return;\n if (viewport.toolManager.activeTool?.name !== tool) {\n viewport.setTool(tool);\n }\n }, [viewport, tool]);\n\n useEffect(() => {\n if (!viewport || !onToolChange) return;\n return viewport.toolManager.onChange(onToolChange);\n }, [viewport, onToolChange]);\n\n useEffect(() => {\n if (!viewport || snapToGrid === undefined) return;\n viewport.setSnapToGrid(snapToGrid);\n }, [viewport, snapToGrid]);\n\n useEffect(() => {\n if (!viewport || !tools) return;\n for (const t of tools) {\n if (!viewport.toolManager.getTool(t.name)) {\n viewport.toolManager.register(t);\n }\n }\n }, [viewport, tools]);\n\n return (\n <div ref={containerRef} className={className} style={style}>\n {viewport && (\n <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>\n )}\n </div>\n );\n },\n);\n","import { createContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\n\r\nexport const ViewportContext = createContext<Viewport | null>(null);\r\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useViewport } from './hooks/use-viewport';\n\n/** Props for {@link CanvasElement}. */\nexport interface CanvasElementProps {\n /** Canvas-space position of the element's top-left corner. */\n position: { x: number; y: number };\n /** Optional fixed size; omit to let the element size to its content. */\n size?: { w: number; h: number };\n children: ReactNode;\n}\n\n/**\n * Renders a React subtree as an HTML element embedded in the canvas coordinate space.\n * Must be used inside a `<FieldNotesCanvas>`.\n */\nexport function CanvasElement({ position, size, children }: CanvasElementProps) {\n const viewport = useViewport();\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n const elementIdRef = useRef<string | null>(null);\n\n useEffect(() => {\n const container = document.createElement('div');\n Object.assign(container.style, {\n width: '100%',\n height: '100%',\n });\n\n // Append to domLayer immediately so portal children are queryable in the document\n // before the viewport render loop fires via requestAnimationFrame.\n viewport.domLayer.appendChild(container);\n\n const id = viewport.addHtmlElement(container, position, size);\n elementIdRef.current = id;\n setPortalTarget(container);\n\n return () => {\n if (elementIdRef.current) {\n viewport.store.remove(elementIdRef.current);\n viewport.requestRender();\n elementIdRef.current = null;\n }\n setPortalTarget(null);\n };\n }, [viewport]);\n\n useEffect(() => {\n const id = elementIdRef.current;\n if (!id) return;\n viewport.store.update(id, { position });\n if (size) {\n viewport.store.update(id, { size });\n }\n viewport.requestRender();\n }, [viewport, position.x, position.y, size?.w, size?.h]);\n\n if (!portalTarget) return null;\n return createPortal(children, portalTarget);\n}\n","import { useContext } from 'react';\nimport type { Viewport } from '@fieldnotes/core';\nimport { ViewportContext } from '../context';\n\n/** The Viewport from the nearest `<FieldNotesCanvas>`. Throws outside one. */\nexport function useViewport(): Viewport {\n const viewport = useContext(ViewportContext);\n if (!viewport) {\n throw new Error('useViewport must be used inside <FieldNotesCanvas>');\n }\n return viewport;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\n/** `[activeToolName, setTool]` — re-renders on tool switches. */\nexport function useActiveTool(): [toolName: string, setTool: (name: string) => void] {\n const viewport = useViewport();\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.toolManager.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? '', [viewport]);\n\n const toolName = useSyncExternalStore(subscribe, getSnapshot);\n\n const setTool = useCallback((name: string) => viewport.setTool(name), [viewport]);\n\n return [toolName, setTool];\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\nexport interface CameraState {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** Current camera position and zoom level — re-renders whenever the camera moves or zoom changes. */\nexport function useCamera(): CameraState {\n const viewport = useViewport();\n const cachedRef = useRef<CameraState>({ x: 0, y: 0, zoom: 1 });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.camera.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CameraState => {\n const { position, zoom } = viewport.camera;\n const cached = cachedRef.current;\n if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {\n return cached;\n }\n const next = { x: position.x, y: position.y, zoom };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\nfunction noop() {\n /* unsubscribe placeholder */\n}\n\n/**\n * `[options, setOptions]` for the named tool — re-renders when the tool's options change.\n * Returns `null` for options if the tool is not registered or does not expose options.\n */\nexport function useToolOptions<T extends Record<string, unknown>>(\n toolName: string,\n): [options: T | null, setOptions: (partial: Partial<T>) => void] {\n const viewport = useViewport();\n const cachedRef = useRef<T | null>(null);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const tool = viewport.toolManager.getTool(toolName);\n if (!tool?.onOptionsChange) return noop;\n return tool.onOptionsChange(onStoreChange);\n },\n [viewport, toolName],\n );\n\n const getSnapshot = useCallback((): T | null => {\n const tool = viewport.toolManager.getTool(toolName);\n if (!tool?.getOptions) return null;\n const next = tool.getOptions() as T;\n const cached = cachedRef.current;\n if (cached && shallowEqual(cached, next)) return cached;\n cachedRef.current = next;\n return next;\n }, [viewport, toolName]);\n\n const options = useSyncExternalStore(subscribe, getSnapshot);\n\n const setOptions = useCallback(\n (partial: Partial<T>) => {\n const tool = viewport.toolManager.getTool(toolName);\n tool?.setOptions?.(partial as Record<string, unknown>);\n },\n [viewport, toolName],\n );\n\n return [options, setOptions];\n}\n\nfunction shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n const valA = a[key];\n const valB = b[key];\n if (valA === valB) continue;\n if (\n typeof valA === 'object' &&\n valA !== null &&\n typeof valB === 'object' &&\n valB !== null &&\n !Array.isArray(valA) &&\n !Array.isArray(valB)\n ) {\n const objA = valA as Record<string, unknown>;\n const objB = valB as Record<string, unknown>;\n const innerKeys = Object.keys(objA);\n if (\n innerKeys.length === Object.keys(objB).length &&\n innerKeys.every((k) => objA[k] === objB[k])\n ) {\n continue;\n }\n }\n return false;\n }\n return true;\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport type { Layer } from '@fieldnotes/core';\nimport { useViewport } from './use-viewport';\n\ninterface LayersSnapshot {\n layers: Layer[];\n activeLayerId: string;\n}\n\nexport interface UseLayersResult {\n layers: Layer[];\n activeLayerId: string;\n createLayer: (name?: string) => Layer;\n removeLayer: (id: string) => void;\n renameLayer: (id: string, name: string) => void;\n reorderLayer: (id: string, newOrder: number) => void;\n setVisible: (id: string, visible: boolean) => void;\n setLocked: (id: string, locked: boolean) => void;\n setOpacity: (id: string, opacity: number) => void;\n setActiveLayer: (id: string) => void;\n moveElement: (elementId: string, layerId: string) => void;\n}\n\n/**\n * Current layer list and mutation helpers — re-renders whenever a layer is added, removed,\n * reordered, renamed, or its visibility/lock/opacity/active state changes.\n */\nexport function useLayers(): UseLayersResult {\n const viewport = useViewport();\n const cachedRef = useRef<LayersSnapshot>({ layers: [], activeLayerId: '' });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.layerManager.on('change', onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): LayersSnapshot => {\n const layers = viewport.layerManager.getLayers();\n const activeLayerId = viewport.layerManager.activeLayerId;\n const cached = cachedRef.current;\n if (\n cached.activeLayerId === activeLayerId &&\n cached.layers.length === layers.length &&\n cached.layers.every((l, i) => {\n const next = layers[i];\n return (\n next !== undefined &&\n l.id === next.id &&\n l.name === next.name &&\n l.visible === next.visible &&\n l.locked === next.locked &&\n l.order === next.order &&\n l.opacity === next.opacity\n );\n })\n ) {\n return cached;\n }\n const next = { layers, activeLayerId };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n\n const createLayer = useCallback(\n (name?: string) => viewport.layerManager.createLayer(name),\n [viewport],\n );\n\n const removeLayer = useCallback(\n (id: string) => viewport.layerManager.removeLayer(id),\n [viewport],\n );\n\n const renameLayer = useCallback(\n (id: string, name: string) => viewport.layerManager.renameLayer(id, name),\n [viewport],\n );\n\n const reorderLayer = useCallback(\n (id: string, newOrder: number) => viewport.layerManager.reorderLayer(id, newOrder),\n [viewport],\n );\n\n const setVisible = useCallback(\n (id: string, visible: boolean) => viewport.layerManager.setLayerVisible(id, visible),\n [viewport],\n );\n\n const setLocked = useCallback(\n (id: string, locked: boolean) => viewport.layerManager.setLayerLocked(id, locked),\n [viewport],\n );\n\n const setOpacity = useCallback(\n (id: string, opacity: number) => viewport.layerManager.setLayerOpacity(id, opacity),\n [viewport],\n );\n\n const setActiveLayer = useCallback(\n (id: string) => viewport.layerManager.setActiveLayer(id),\n [viewport],\n );\n\n const moveElement = useCallback(\n (elementId: string, layerId: string) =>\n viewport.layerManager.moveElementToLayer(elementId, layerId),\n [viewport],\n );\n\n return {\n layers: snapshot.layers,\n activeLayerId: snapshot.activeLayerId,\n createLayer,\n removeLayer,\n renameLayer,\n reorderLayer,\n setVisible,\n setLocked,\n setOpacity,\n setActiveLayer,\n moveElement,\n };\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\ninterface HistorySnapshot {\n canUndo: boolean;\n canRedo: boolean;\n}\n\nexport interface UseHistoryResult {\n canUndo: boolean;\n canRedo: boolean;\n undo: () => void;\n redo: () => void;\n}\n\n/** `{ canUndo, canRedo, undo, redo }` — re-renders when undo/redo availability changes. */\nexport function useHistory(): UseHistoryResult {\n const viewport = useViewport();\n const cachedRef = useRef<HistorySnapshot>({ canUndo: false, canRedo: false });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.history.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): HistorySnapshot => {\n const canUndo = viewport.history.canUndo;\n const canRedo = viewport.history.canRedo;\n const cached = cachedRef.current;\n if (cached.canUndo === canUndo && cached.canRedo === canRedo) {\n return cached;\n }\n const next = { canUndo, canRedo };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n\n const undo = useCallback(() => viewport.undo(), [viewport]);\n const redo = useCallback(() => viewport.redo(), [viewport]);\n\n return {\n canUndo: snapshot.canUndo,\n canRedo: snapshot.canRedo,\n undo,\n redo,\n };\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport type { CanvasElement, ElementType } from '@fieldnotes/core';\nimport { useViewport } from './use-viewport';\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));\n }\n if (\n typeof a === 'object' &&\n a !== null &&\n typeof b === 'object' &&\n b !== null &&\n !Array.isArray(a) &&\n !Array.isArray(b)\n ) {\n const ka = Object.keys(a);\n const kb = Object.keys(b);\n return (\n ka.length === kb.length &&\n ka.every(\n (k) =>\n k in (b as Record<string, unknown>) &&\n Object.is((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),\n )\n );\n }\n return false;\n}\n\n/** All elements; re-renders on every store mutation (use a selector for hot paths). */\nexport function useElements(): CanvasElement[];\n/** Elements of one type; re-renders on every store mutation. */\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\n/**\n * Derived value; re-renders only when the selected value changes (per `isEqual`,\n * default: one-level shallow compare — arrays by index, plain objects by key, primitives by\n * Object.is). Keep the selector referentially stable (useCallback or module scope).\n *\n * Supply `isEqual` for deeper structures. Note: selectors returning nested fresh objects\n * (e.g. `els => els.map(e => ({ ...e.position }))`) still need a custom comparator because\n * the default only compares one level deep.\n */\nexport function useElements<R>(\n selector: (elements: CanvasElement[]) => R,\n isEqual?: (a: R, b: R) => boolean,\n): R;\nexport function useElements<R>(\n arg?: ElementType | ((elements: CanvasElement[]) => R),\n isEqual?: (a: R, b: R) => boolean,\n): CanvasElement[] | R {\n const viewport = useViewport();\n const cachedRef = useRef<CanvasElement[]>([]);\n const selectedRef = useRef<{ value: R } | null>(null);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CanvasElement[] | R => {\n if (typeof arg === 'function') {\n const next = arg(viewport.store.getAll());\n const prev = selectedRef.current;\n const equal: (a: R, b: R) => boolean = isEqual ?? (shallowEqual as (a: R, b: R) => boolean);\n if (prev !== null && equal(prev.value, next)) {\n return prev.value;\n }\n selectedRef.current = { value: next };\n return next;\n }\n\n const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();\n const cached = cachedRef.current;\n if (cached.length === next.length && cached.every((el, i) => el === next[i])) {\n return cached;\n }\n cachedRef.current = next;\n return next;\n }, [viewport, arg, isEqual]);\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n"],"mappings":"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","names":["import_react","FieldNotesCanvas","import_react","import_react","import_react","import_react","import_react","import_react","next","import_react","import_react","shallowEqual","next"]}
package/dist/index.d.cts CHANGED
@@ -3,9 +3,25 @@ import { CSSProperties, ReactNode } from 'react';
3
3
  import { ViewportOptions, Tool, Viewport, Layer, CanvasElement as CanvasElement$1, ElementType } from '@fieldnotes/core';
4
4
 
5
5
  interface FieldNotesCanvasProps {
6
+ /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
6
7
  options?: ViewportOptions;
8
+ /**
9
+ * Reactive (append-only): tools present here are registered; removal is not supported.
10
+ * Hoist the array out of render — inline arrays reconstruct Tool instances and re-run
11
+ * registration on every render.
12
+ */
7
13
  tools?: Tool[];
14
+ /** Uncontrolled initial tool. Ignored when `tool` is set. */
8
15
  defaultTool?: string;
16
+ /** Controlled active tool. Pair with `onToolChange`. */
17
+ tool?: string;
18
+ /**
19
+ * Fires whenever the active tool changes (keyboard, API, or `tool` prop).
20
+ * Memoize with useCallback to avoid re-subscribing each render.
21
+ */
22
+ onToolChange?: (name: string) => void;
23
+ /** Reactive: toggles grid snapping. */
24
+ snapToGrid?: boolean;
9
25
  className?: string;
10
26
  style?: CSSProperties;
11
27
  children?: ReactNode;
@@ -16,21 +32,30 @@ interface FieldNotesCanvasRef {
16
32
  }
17
33
  declare const FieldNotesCanvas: react.ForwardRefExoticComponent<FieldNotesCanvasProps & react.RefAttributes<FieldNotesCanvasRef>>;
18
34
 
35
+ /** Props for {@link CanvasElement}. */
19
36
  interface CanvasElementProps {
37
+ /** Canvas-space position of the element's top-left corner. */
20
38
  position: {
21
39
  x: number;
22
40
  y: number;
23
41
  };
42
+ /** Optional fixed size; omit to let the element size to its content. */
24
43
  size?: {
25
44
  w: number;
26
45
  h: number;
27
46
  };
28
47
  children: ReactNode;
29
48
  }
49
+ /**
50
+ * Renders a React subtree as an HTML element embedded in the canvas coordinate space.
51
+ * Must be used inside a `<FieldNotesCanvas>`.
52
+ */
30
53
  declare function CanvasElement({ position, size, children }: CanvasElementProps): react.ReactPortal | null;
31
54
 
55
+ /** The Viewport from the nearest `<FieldNotesCanvas>`. Throws outside one. */
32
56
  declare function useViewport(): Viewport;
33
57
 
58
+ /** `[activeToolName, setTool]` — re-renders on tool switches. */
34
59
  declare function useActiveTool(): [toolName: string, setTool: (name: string) => void];
35
60
 
36
61
  interface CameraState {
@@ -38,8 +63,13 @@ interface CameraState {
38
63
  y: number;
39
64
  zoom: number;
40
65
  }
66
+ /** Current camera position and zoom level — re-renders whenever the camera moves or zoom changes. */
41
67
  declare function useCamera(): CameraState;
42
68
 
69
+ /**
70
+ * `[options, setOptions]` for the named tool — re-renders when the tool's options change.
71
+ * Returns `null` for options if the tool is not registered or does not expose options.
72
+ */
43
73
  declare function useToolOptions<T extends Record<string, unknown>>(toolName: string): [options: T | null, setOptions: (partial: Partial<T>) => void];
44
74
 
45
75
  interface UseLayersResult {
@@ -55,6 +85,10 @@ interface UseLayersResult {
55
85
  setActiveLayer: (id: string) => void;
56
86
  moveElement: (elementId: string, layerId: string) => void;
57
87
  }
88
+ /**
89
+ * Current layer list and mutation helpers — re-renders whenever a layer is added, removed,
90
+ * reordered, renamed, or its visibility/lock/opacity/active state changes.
91
+ */
58
92
  declare function useLayers(): UseLayersResult;
59
93
 
60
94
  interface UseHistoryResult {
@@ -63,12 +97,25 @@ interface UseHistoryResult {
63
97
  undo: () => void;
64
98
  redo: () => void;
65
99
  }
100
+ /** `{ canUndo, canRedo, undo, redo }` — re-renders when undo/redo availability changes. */
66
101
  declare function useHistory(): UseHistoryResult;
67
102
 
103
+ /** All elements; re-renders on every store mutation (use a selector for hot paths). */
68
104
  declare function useElements(): CanvasElement$1[];
105
+ /** Elements of one type; re-renders on every store mutation. */
69
106
  declare function useElements<T extends ElementType>(type: T): Extract<CanvasElement$1, {
70
107
  type: T;
71
108
  }>[];
109
+ /**
110
+ * Derived value; re-renders only when the selected value changes (per `isEqual`,
111
+ * default: one-level shallow compare — arrays by index, plain objects by key, primitives by
112
+ * Object.is). Keep the selector referentially stable (useCallback or module scope).
113
+ *
114
+ * Supply `isEqual` for deeper structures. Note: selectors returning nested fresh objects
115
+ * (e.g. `els => els.map(e => ({ ...e.position }))`) still need a custom comparator because
116
+ * the default only compares one level deep.
117
+ */
118
+ declare function useElements<R>(selector: (elements: CanvasElement$1[]) => R, isEqual?: (a: R, b: R) => boolean): R;
72
119
 
73
120
  declare const ViewportContext: react.Context<Viewport | null>;
74
121
 
package/dist/index.d.ts CHANGED
@@ -3,9 +3,25 @@ import { CSSProperties, ReactNode } from 'react';
3
3
  import { ViewportOptions, Tool, Viewport, Layer, CanvasElement as CanvasElement$1, ElementType } from '@fieldnotes/core';
4
4
 
5
5
  interface FieldNotesCanvasProps {
6
+ /** Constructor-only: changes after mount are ignored (the canvas is stateful). */
6
7
  options?: ViewportOptions;
8
+ /**
9
+ * Reactive (append-only): tools present here are registered; removal is not supported.
10
+ * Hoist the array out of render — inline arrays reconstruct Tool instances and re-run
11
+ * registration on every render.
12
+ */
7
13
  tools?: Tool[];
14
+ /** Uncontrolled initial tool. Ignored when `tool` is set. */
8
15
  defaultTool?: string;
16
+ /** Controlled active tool. Pair with `onToolChange`. */
17
+ tool?: string;
18
+ /**
19
+ * Fires whenever the active tool changes (keyboard, API, or `tool` prop).
20
+ * Memoize with useCallback to avoid re-subscribing each render.
21
+ */
22
+ onToolChange?: (name: string) => void;
23
+ /** Reactive: toggles grid snapping. */
24
+ snapToGrid?: boolean;
9
25
  className?: string;
10
26
  style?: CSSProperties;
11
27
  children?: ReactNode;
@@ -16,21 +32,30 @@ interface FieldNotesCanvasRef {
16
32
  }
17
33
  declare const FieldNotesCanvas: react.ForwardRefExoticComponent<FieldNotesCanvasProps & react.RefAttributes<FieldNotesCanvasRef>>;
18
34
 
35
+ /** Props for {@link CanvasElement}. */
19
36
  interface CanvasElementProps {
37
+ /** Canvas-space position of the element's top-left corner. */
20
38
  position: {
21
39
  x: number;
22
40
  y: number;
23
41
  };
42
+ /** Optional fixed size; omit to let the element size to its content. */
24
43
  size?: {
25
44
  w: number;
26
45
  h: number;
27
46
  };
28
47
  children: ReactNode;
29
48
  }
49
+ /**
50
+ * Renders a React subtree as an HTML element embedded in the canvas coordinate space.
51
+ * Must be used inside a `<FieldNotesCanvas>`.
52
+ */
30
53
  declare function CanvasElement({ position, size, children }: CanvasElementProps): react.ReactPortal | null;
31
54
 
55
+ /** The Viewport from the nearest `<FieldNotesCanvas>`. Throws outside one. */
32
56
  declare function useViewport(): Viewport;
33
57
 
58
+ /** `[activeToolName, setTool]` — re-renders on tool switches. */
34
59
  declare function useActiveTool(): [toolName: string, setTool: (name: string) => void];
35
60
 
36
61
  interface CameraState {
@@ -38,8 +63,13 @@ interface CameraState {
38
63
  y: number;
39
64
  zoom: number;
40
65
  }
66
+ /** Current camera position and zoom level — re-renders whenever the camera moves or zoom changes. */
41
67
  declare function useCamera(): CameraState;
42
68
 
69
+ /**
70
+ * `[options, setOptions]` for the named tool — re-renders when the tool's options change.
71
+ * Returns `null` for options if the tool is not registered or does not expose options.
72
+ */
43
73
  declare function useToolOptions<T extends Record<string, unknown>>(toolName: string): [options: T | null, setOptions: (partial: Partial<T>) => void];
44
74
 
45
75
  interface UseLayersResult {
@@ -55,6 +85,10 @@ interface UseLayersResult {
55
85
  setActiveLayer: (id: string) => void;
56
86
  moveElement: (elementId: string, layerId: string) => void;
57
87
  }
88
+ /**
89
+ * Current layer list and mutation helpers — re-renders whenever a layer is added, removed,
90
+ * reordered, renamed, or its visibility/lock/opacity/active state changes.
91
+ */
58
92
  declare function useLayers(): UseLayersResult;
59
93
 
60
94
  interface UseHistoryResult {
@@ -63,12 +97,25 @@ interface UseHistoryResult {
63
97
  undo: () => void;
64
98
  redo: () => void;
65
99
  }
100
+ /** `{ canUndo, canRedo, undo, redo }` — re-renders when undo/redo availability changes. */
66
101
  declare function useHistory(): UseHistoryResult;
67
102
 
103
+ /** All elements; re-renders on every store mutation (use a selector for hot paths). */
68
104
  declare function useElements(): CanvasElement$1[];
105
+ /** Elements of one type; re-renders on every store mutation. */
69
106
  declare function useElements<T extends ElementType>(type: T): Extract<CanvasElement$1, {
70
107
  type: T;
71
108
  }>[];
109
+ /**
110
+ * Derived value; re-renders only when the selected value changes (per `isEqual`,
111
+ * default: one-level shallow compare — arrays by index, plain objects by key, primitives by
112
+ * Object.is). Keep the selector referentially stable (useCallback or module scope).
113
+ *
114
+ * Supply `isEqual` for deeper structures. Note: selectors returning nested fresh objects
115
+ * (e.g. `els => els.map(e => ({ ...e.position }))`) still need a custom comparator because
116
+ * the default only compares one level deep.
117
+ */
118
+ declare function useElements<R>(selector: (elements: CanvasElement$1[]) => R, isEqual?: (a: R, b: R) => boolean): R;
72
119
 
73
120
  declare const ViewportContext: react.Context<Viewport | null>;
74
121
 
package/dist/index.js CHANGED
@@ -15,7 +15,18 @@ var ViewportContext = createContext(null);
15
15
  // src/field-notes-canvas.tsx
16
16
  import { jsx } from "react/jsx-runtime";
17
17
  var FieldNotesCanvas = forwardRef(
18
- function FieldNotesCanvas2({ options, tools, defaultTool, className, style, children, onReady }, ref) {
18
+ function FieldNotesCanvas2({
19
+ options,
20
+ tools,
21
+ defaultTool,
22
+ tool,
23
+ onToolChange,
24
+ snapToGrid,
25
+ className,
26
+ style,
27
+ children,
28
+ onReady
29
+ }, ref) {
19
30
  const containerRef = useRef(null);
20
31
  const [viewport, setViewport] = useState(null);
21
32
  useImperativeHandle(ref, () => ({ viewport }), [viewport]);
@@ -24,12 +35,13 @@ var FieldNotesCanvas = forwardRef(
24
35
  if (!el) return;
25
36
  const vp = new Viewport(el, options);
26
37
  if (tools) {
27
- for (const tool of tools) {
28
- vp.toolManager.register(tool);
38
+ for (const t of tools) {
39
+ vp.toolManager.register(t);
29
40
  }
30
41
  }
31
- if (defaultTool) {
32
- vp.toolManager.setTool(defaultTool, vp.toolContext);
42
+ const initialTool = tool ?? defaultTool;
43
+ if (initialTool) {
44
+ vp.setTool(initialTool);
33
45
  }
34
46
  setViewport(vp);
35
47
  onReady?.(vp);
@@ -38,6 +50,28 @@ var FieldNotesCanvas = forwardRef(
38
50
  setViewport(null);
39
51
  };
40
52
  }, []);
53
+ useEffect(() => {
54
+ if (!viewport || tool === void 0) return;
55
+ if (viewport.toolManager.activeTool?.name !== tool) {
56
+ viewport.setTool(tool);
57
+ }
58
+ }, [viewport, tool]);
59
+ useEffect(() => {
60
+ if (!viewport || !onToolChange) return;
61
+ return viewport.toolManager.onChange(onToolChange);
62
+ }, [viewport, onToolChange]);
63
+ useEffect(() => {
64
+ if (!viewport || snapToGrid === void 0) return;
65
+ viewport.setSnapToGrid(snapToGrid);
66
+ }, [viewport, snapToGrid]);
67
+ useEffect(() => {
68
+ if (!viewport || !tools) return;
69
+ for (const t of tools) {
70
+ if (!viewport.toolManager.getTool(t.name)) {
71
+ viewport.toolManager.register(t);
72
+ }
73
+ }
74
+ }, [viewport, tools]);
41
75
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style, children: viewport && /* @__PURE__ */ jsx(ViewportContext.Provider, { value: viewport, children }) });
42
76
  }
43
77
  );
@@ -98,15 +132,12 @@ import { useCallback, useSyncExternalStore } from "react";
98
132
  function useActiveTool() {
99
133
  const viewport = useViewport();
100
134
  const subscribe = useCallback(
101
- (onStoreChange) => viewport.toolManager.onChange(() => onStoreChange()),
135
+ (onStoreChange) => viewport.toolManager.onChange(onStoreChange),
102
136
  [viewport]
103
137
  );
104
138
  const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
105
139
  const toolName = useSyncExternalStore(subscribe, getSnapshot);
106
- const setTool = useCallback(
107
- (name) => viewport.toolManager.setTool(name, viewport.toolContext),
108
- [viewport]
109
- );
140
+ const setTool = useCallback((name) => viewport.setTool(name), [viewport]);
110
141
  return [toolName, setTool];
111
142
  }
112
143
 
@@ -295,22 +326,47 @@ function useHistory() {
295
326
 
296
327
  // src/hooks/use-elements.ts
297
328
  import { useCallback as useCallback6, useRef as useRef7, useSyncExternalStore as useSyncExternalStore6 } from "react";
298
- function useElements(type) {
329
+ function shallowEqual2(a, b) {
330
+ if (Object.is(a, b)) return true;
331
+ if (Array.isArray(a) && Array.isArray(b)) {
332
+ return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
333
+ }
334
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
335
+ const ka = Object.keys(a);
336
+ const kb = Object.keys(b);
337
+ return ka.length === kb.length && ka.every(
338
+ (k) => k in b && Object.is(a[k], b[k])
339
+ );
340
+ }
341
+ return false;
342
+ }
343
+ function useElements(arg, isEqual) {
299
344
  const viewport = useViewport();
300
345
  const cachedRef = useRef7([]);
346
+ const selectedRef = useRef7(null);
301
347
  const subscribe = useCallback6(
302
348
  (onStoreChange) => viewport.store.onChange(onStoreChange),
303
349
  [viewport]
304
350
  );
305
351
  const getSnapshot = useCallback6(() => {
306
- const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();
352
+ if (typeof arg === "function") {
353
+ const next2 = arg(viewport.store.getAll());
354
+ const prev = selectedRef.current;
355
+ const equal = isEqual ?? shallowEqual2;
356
+ if (prev !== null && equal(prev.value, next2)) {
357
+ return prev.value;
358
+ }
359
+ selectedRef.current = { value: next2 };
360
+ return next2;
361
+ }
362
+ const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
307
363
  const cached = cachedRef.current;
308
364
  if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
309
365
  return cached;
310
366
  }
311
367
  cachedRef.current = next;
312
368
  return next;
313
- }, [viewport, type]);
369
+ }, [viewport, arg, isEqual]);
314
370
  return useSyncExternalStore6(subscribe, getSnapshot);
315
371
  }
316
372
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/field-notes-canvas.tsx","../src/context.ts","../src/canvas-element.tsx","../src/hooks/use-viewport.ts","../src/hooks/use-active-tool.ts","../src/hooks/use-camera.ts","../src/hooks/use-tool-options.ts","../src/hooks/use-layers.ts","../src/hooks/use-history.ts","../src/hooks/use-elements.ts"],"sourcesContent":["import {\r\n useRef,\r\n useEffect,\r\n useState,\r\n forwardRef,\r\n useImperativeHandle,\r\n type ReactNode,\r\n type CSSProperties,\r\n} from 'react';\r\nimport { Viewport } from '@fieldnotes/core';\r\nimport type { ViewportOptions, Tool } from '@fieldnotes/core';\r\nimport { ViewportContext } from './context';\r\n\r\nexport interface FieldNotesCanvasProps {\r\n options?: ViewportOptions;\r\n tools?: Tool[];\r\n defaultTool?: string;\r\n className?: string;\r\n style?: CSSProperties;\r\n children?: ReactNode;\r\n onReady?: (viewport: Viewport) => void;\r\n}\r\n\r\nexport interface FieldNotesCanvasRef {\r\n viewport: Viewport | null;\r\n}\r\n\r\nexport const FieldNotesCanvas = forwardRef<FieldNotesCanvasRef, FieldNotesCanvasProps>(\r\n function FieldNotesCanvas(\r\n { options, tools, defaultTool, className, style, children, onReady },\r\n ref,\r\n ) {\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const [viewport, setViewport] = useState<Viewport | null>(null);\r\n\r\n useImperativeHandle(ref, () => ({ viewport }), [viewport]);\r\n\r\n useEffect(() => {\r\n const el = containerRef.current;\r\n if (!el) return;\r\n\r\n const vp = new Viewport(el, options);\r\n\r\n if (tools) {\r\n for (const tool of tools) {\r\n vp.toolManager.register(tool);\r\n }\r\n }\r\n\r\n if (defaultTool) {\r\n vp.toolManager.setTool(defaultTool, vp.toolContext);\r\n }\r\n\r\n setViewport(vp);\r\n onReady?.(vp);\r\n\r\n return () => {\r\n vp.destroy();\r\n setViewport(null);\r\n };\r\n }, []);\r\n\r\n return (\r\n <div ref={containerRef} className={className} style={style}>\r\n {viewport && (\r\n <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\n","import { createContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\n\r\nexport const ViewportContext = createContext<Viewport | null>(null);\r\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\r\nimport { createPortal } from 'react-dom';\r\nimport { useViewport } from './hooks/use-viewport';\r\n\r\nexport interface CanvasElementProps {\r\n position: { x: number; y: number };\r\n size?: { w: number; h: number };\r\n children: ReactNode;\r\n}\r\n\r\nexport function CanvasElement({ position, size, children }: CanvasElementProps) {\r\n const viewport = useViewport();\r\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\r\n const elementIdRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n const container = document.createElement('div');\r\n Object.assign(container.style, {\r\n width: '100%',\r\n height: '100%',\r\n });\r\n\r\n // Append to domLayer immediately so portal children are queryable in the document\r\n // before the viewport render loop fires via requestAnimationFrame.\r\n viewport.domLayer.appendChild(container);\r\n\r\n const id = viewport.addHtmlElement(container, position, size);\r\n elementIdRef.current = id;\r\n setPortalTarget(container);\r\n\r\n return () => {\r\n if (elementIdRef.current) {\r\n viewport.store.remove(elementIdRef.current);\r\n viewport.requestRender();\r\n elementIdRef.current = null;\r\n }\r\n setPortalTarget(null);\r\n };\r\n }, [viewport]);\r\n\r\n useEffect(() => {\r\n const id = elementIdRef.current;\r\n if (!id) return;\r\n viewport.store.update(id, { position });\r\n if (size) {\r\n viewport.store.update(id, { size });\r\n }\r\n viewport.requestRender();\r\n }, [viewport, position.x, position.y, size?.w, size?.h]);\r\n\r\n if (!portalTarget) return null;\r\n return createPortal(children, portalTarget);\r\n}\r\n","import { useContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\nimport { ViewportContext } from '../context';\r\n\r\nexport function useViewport(): Viewport {\r\n const viewport = useContext(ViewportContext);\r\n if (!viewport) {\r\n throw new Error('useViewport must be used inside <FieldNotesCanvas>');\r\n }\r\n return viewport;\r\n}\r\n","import { useCallback, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport function useActiveTool(): [toolName: string, setTool: (name: string) => void] {\r\n const viewport = useViewport();\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.toolManager.onChange(() => onStoreChange()),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? '', [viewport]);\r\n\r\n const toolName = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const setTool = useCallback(\r\n (name: string) => viewport.toolManager.setTool(name, viewport.toolContext),\r\n [viewport],\r\n );\r\n\r\n return [toolName, setTool];\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport interface CameraState {\r\n x: number;\r\n y: number;\r\n zoom: number;\r\n}\r\n\r\nexport function useCamera(): CameraState {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<CameraState>({ x: 0, y: 0, zoom: 1 });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.camera.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): CameraState => {\r\n const { position, zoom } = viewport.camera;\r\n const cached = cachedRef.current;\r\n if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {\r\n return cached;\r\n }\r\n const next = { x: position.x, y: position.y, zoom };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\nfunction noop() {\r\n /* unsubscribe placeholder */\r\n}\r\n\r\nexport function useToolOptions<T extends Record<string, unknown>>(\r\n toolName: string,\r\n): [options: T | null, setOptions: (partial: Partial<T>) => void] {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<T | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n if (!tool?.onOptionsChange) return noop;\r\n return tool.onOptionsChange(onStoreChange);\r\n },\r\n [viewport, toolName],\r\n );\r\n\r\n const getSnapshot = useCallback((): T | null => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n if (!tool?.getOptions) return null;\r\n const next = tool.getOptions() as T;\r\n const cached = cachedRef.current;\r\n if (cached && shallowEqual(cached, next)) return cached;\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport, toolName]);\r\n\r\n const options = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const setOptions = useCallback(\r\n (partial: Partial<T>) => {\r\n const tool = viewport.toolManager.getTool(toolName);\r\n tool?.setOptions?.(partial as Record<string, unknown>);\r\n },\r\n [viewport, toolName],\r\n );\r\n\r\n return [options, setOptions];\r\n}\r\n\r\nfunction shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\r\n const keysA = Object.keys(a);\r\n const keysB = Object.keys(b);\r\n if (keysA.length !== keysB.length) return false;\r\n for (const key of keysA) {\r\n const valA = a[key];\r\n const valB = b[key];\r\n if (valA === valB) continue;\r\n if (\r\n typeof valA === 'object' &&\r\n valA !== null &&\r\n typeof valB === 'object' &&\r\n valB !== null &&\r\n !Array.isArray(valA) &&\r\n !Array.isArray(valB)\r\n ) {\r\n const objA = valA as Record<string, unknown>;\r\n const objB = valB as Record<string, unknown>;\r\n const innerKeys = Object.keys(objA);\r\n if (\r\n innerKeys.length === Object.keys(objB).length &&\r\n innerKeys.every((k) => objA[k] === objB[k])\r\n ) {\r\n continue;\r\n }\r\n }\r\n return false;\r\n }\r\n return true;\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport type { Layer } from '@fieldnotes/core';\r\nimport { useViewport } from './use-viewport';\r\n\r\ninterface LayersSnapshot {\r\n layers: Layer[];\r\n activeLayerId: string;\r\n}\r\n\r\nexport interface UseLayersResult {\r\n layers: Layer[];\r\n activeLayerId: string;\r\n createLayer: (name?: string) => Layer;\r\n removeLayer: (id: string) => void;\r\n renameLayer: (id: string, name: string) => void;\r\n reorderLayer: (id: string, newOrder: number) => void;\r\n setVisible: (id: string, visible: boolean) => void;\r\n setLocked: (id: string, locked: boolean) => void;\r\n setOpacity: (id: string, opacity: number) => void;\r\n setActiveLayer: (id: string) => void;\r\n moveElement: (elementId: string, layerId: string) => void;\r\n}\r\n\r\nexport function useLayers(): UseLayersResult {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<LayersSnapshot>({ layers: [], activeLayerId: '' });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.layerManager.on('change', onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): LayersSnapshot => {\r\n const layers = viewport.layerManager.getLayers();\r\n const activeLayerId = viewport.layerManager.activeLayerId;\r\n const cached = cachedRef.current;\r\n if (\r\n cached.activeLayerId === activeLayerId &&\r\n cached.layers.length === layers.length &&\r\n cached.layers.every((l, i) => {\r\n const next = layers[i];\r\n return (\r\n next !== undefined &&\r\n l.id === next.id &&\r\n l.name === next.name &&\r\n l.visible === next.visible &&\r\n l.locked === next.locked &&\r\n l.order === next.order &&\r\n l.opacity === next.opacity\r\n );\r\n })\r\n ) {\r\n return cached;\r\n }\r\n const next = { layers, activeLayerId };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const createLayer = useCallback(\r\n (name?: string) => viewport.layerManager.createLayer(name),\r\n [viewport],\r\n );\r\n\r\n const removeLayer = useCallback(\r\n (id: string) => viewport.layerManager.removeLayer(id),\r\n [viewport],\r\n );\r\n\r\n const renameLayer = useCallback(\r\n (id: string, name: string) => viewport.layerManager.renameLayer(id, name),\r\n [viewport],\r\n );\r\n\r\n const reorderLayer = useCallback(\r\n (id: string, newOrder: number) => viewport.layerManager.reorderLayer(id, newOrder),\r\n [viewport],\r\n );\r\n\r\n const setVisible = useCallback(\r\n (id: string, visible: boolean) => viewport.layerManager.setLayerVisible(id, visible),\r\n [viewport],\r\n );\r\n\r\n const setLocked = useCallback(\r\n (id: string, locked: boolean) => viewport.layerManager.setLayerLocked(id, locked),\r\n [viewport],\r\n );\r\n\r\n const setOpacity = useCallback(\r\n (id: string, opacity: number) => viewport.layerManager.setLayerOpacity(id, opacity),\r\n [viewport],\r\n );\r\n\r\n const setActiveLayer = useCallback(\r\n (id: string) => viewport.layerManager.setActiveLayer(id),\r\n [viewport],\r\n );\r\n\r\n const moveElement = useCallback(\r\n (elementId: string, layerId: string) =>\r\n viewport.layerManager.moveElementToLayer(elementId, layerId),\r\n [viewport],\r\n );\r\n\r\n return {\r\n layers: snapshot.layers,\r\n activeLayerId: snapshot.activeLayerId,\r\n createLayer,\r\n removeLayer,\r\n renameLayer,\r\n reorderLayer,\r\n setVisible,\r\n setLocked,\r\n setOpacity,\r\n setActiveLayer,\r\n moveElement,\r\n };\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useViewport } from './use-viewport';\r\n\r\ninterface HistorySnapshot {\r\n canUndo: boolean;\r\n canRedo: boolean;\r\n}\r\n\r\nexport interface UseHistoryResult {\r\n canUndo: boolean;\r\n canRedo: boolean;\r\n undo: () => void;\r\n redo: () => void;\r\n}\r\n\r\nexport function useHistory(): UseHistoryResult {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<HistorySnapshot>({ canUndo: false, canRedo: false });\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.history.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): HistorySnapshot => {\r\n const canUndo = viewport.history.canUndo;\r\n const canRedo = viewport.history.canRedo;\r\n const cached = cachedRef.current;\r\n if (cached.canUndo === canUndo && cached.canRedo === canRedo) {\r\n return cached;\r\n }\r\n const next = { canUndo, canRedo };\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport]);\r\n\r\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\r\n\r\n const undo = useCallback(() => viewport.undo(), [viewport]);\r\n const redo = useCallback(() => viewport.redo(), [viewport]);\r\n\r\n return {\r\n canUndo: snapshot.canUndo,\r\n canRedo: snapshot.canRedo,\r\n undo,\r\n redo,\r\n };\r\n}\r\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport type { CanvasElement, ElementType } from '@fieldnotes/core';\r\nimport { useViewport } from './use-viewport';\r\n\r\nexport function useElements(): CanvasElement[];\r\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\r\nexport function useElements(type?: ElementType): CanvasElement[] {\r\n const viewport = useViewport();\r\n const cachedRef = useRef<CanvasElement[]>([]);\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\r\n [viewport],\r\n );\r\n\r\n const getSnapshot = useCallback((): CanvasElement[] => {\r\n const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();\r\n const cached = cachedRef.current;\r\n if (cached.length === next.length && cached.every((el, i) => el === next[i])) {\r\n return cached;\r\n }\r\n cachedRef.current = next;\r\n return next;\r\n }, [viewport, type]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"],"mappings":"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","names":["FieldNotesCanvas","useEffect","useRef","useState","useState","useRef","useEffect","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","next","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore"]}
1
+ {"version":3,"sources":["../src/field-notes-canvas.tsx","../src/context.ts","../src/canvas-element.tsx","../src/hooks/use-viewport.ts","../src/hooks/use-active-tool.ts","../src/hooks/use-camera.ts","../src/hooks/use-tool-options.ts","../src/hooks/use-layers.ts","../src/hooks/use-history.ts","../src/hooks/use-elements.ts"],"sourcesContent":["import {\n useRef,\n useEffect,\n useState,\n forwardRef,\n useImperativeHandle,\n type ReactNode,\n type CSSProperties,\n} from 'react';\nimport { Viewport } from '@fieldnotes/core';\nimport type { ViewportOptions, Tool } from '@fieldnotes/core';\nimport { ViewportContext } from './context';\n\nexport interface FieldNotesCanvasProps {\n /** Constructor-only: changes after mount are ignored (the canvas is stateful). */\n options?: ViewportOptions;\n /**\n * Reactive (append-only): tools present here are registered; removal is not supported.\n * Hoist the array out of render — inline arrays reconstruct Tool instances and re-run\n * registration on every render.\n */\n tools?: Tool[];\n /** Uncontrolled initial tool. Ignored when `tool` is set. */\n defaultTool?: string;\n /** Controlled active tool. Pair with `onToolChange`. */\n tool?: string;\n /**\n * Fires whenever the active tool changes (keyboard, API, or `tool` prop).\n * Memoize with useCallback to avoid re-subscribing each render.\n */\n onToolChange?: (name: string) => void;\n /** Reactive: toggles grid snapping. */\n snapToGrid?: boolean;\n className?: string;\n style?: CSSProperties;\n children?: ReactNode;\n onReady?: (viewport: Viewport) => void;\n}\n\nexport interface FieldNotesCanvasRef {\n viewport: Viewport | null;\n}\n\nexport const FieldNotesCanvas = forwardRef<FieldNotesCanvasRef, FieldNotesCanvasProps>(\n function FieldNotesCanvas(\n {\n options,\n tools,\n defaultTool,\n tool,\n onToolChange,\n snapToGrid,\n className,\n style,\n children,\n onReady,\n },\n ref,\n ) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [viewport, setViewport] = useState<Viewport | null>(null);\n\n useImperativeHandle(ref, () => ({ viewport }), [viewport]);\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n\n const vp = new Viewport(el, options);\n\n if (tools) {\n for (const t of tools) {\n vp.toolManager.register(t);\n }\n }\n\n const initialTool = tool ?? defaultTool;\n if (initialTool) {\n vp.setTool(initialTool);\n }\n\n setViewport(vp);\n onReady?.(vp);\n\n return () => {\n vp.destroy();\n setViewport(null);\n };\n // Constructor effect: options/tools/defaultTool are mount-only by design.\n }, []);\n\n useEffect(() => {\n if (!viewport || tool === undefined) return;\n if (viewport.toolManager.activeTool?.name !== tool) {\n viewport.setTool(tool);\n }\n }, [viewport, tool]);\n\n useEffect(() => {\n if (!viewport || !onToolChange) return;\n return viewport.toolManager.onChange(onToolChange);\n }, [viewport, onToolChange]);\n\n useEffect(() => {\n if (!viewport || snapToGrid === undefined) return;\n viewport.setSnapToGrid(snapToGrid);\n }, [viewport, snapToGrid]);\n\n useEffect(() => {\n if (!viewport || !tools) return;\n for (const t of tools) {\n if (!viewport.toolManager.getTool(t.name)) {\n viewport.toolManager.register(t);\n }\n }\n }, [viewport, tools]);\n\n return (\n <div ref={containerRef} className={className} style={style}>\n {viewport && (\n <ViewportContext.Provider value={viewport}>{children}</ViewportContext.Provider>\n )}\n </div>\n );\n },\n);\n","import { createContext } from 'react';\r\nimport type { Viewport } from '@fieldnotes/core';\r\n\r\nexport const ViewportContext = createContext<Viewport | null>(null);\r\n","import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useViewport } from './hooks/use-viewport';\n\n/** Props for {@link CanvasElement}. */\nexport interface CanvasElementProps {\n /** Canvas-space position of the element's top-left corner. */\n position: { x: number; y: number };\n /** Optional fixed size; omit to let the element size to its content. */\n size?: { w: number; h: number };\n children: ReactNode;\n}\n\n/**\n * Renders a React subtree as an HTML element embedded in the canvas coordinate space.\n * Must be used inside a `<FieldNotesCanvas>`.\n */\nexport function CanvasElement({ position, size, children }: CanvasElementProps) {\n const viewport = useViewport();\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n const elementIdRef = useRef<string | null>(null);\n\n useEffect(() => {\n const container = document.createElement('div');\n Object.assign(container.style, {\n width: '100%',\n height: '100%',\n });\n\n // Append to domLayer immediately so portal children are queryable in the document\n // before the viewport render loop fires via requestAnimationFrame.\n viewport.domLayer.appendChild(container);\n\n const id = viewport.addHtmlElement(container, position, size);\n elementIdRef.current = id;\n setPortalTarget(container);\n\n return () => {\n if (elementIdRef.current) {\n viewport.store.remove(elementIdRef.current);\n viewport.requestRender();\n elementIdRef.current = null;\n }\n setPortalTarget(null);\n };\n }, [viewport]);\n\n useEffect(() => {\n const id = elementIdRef.current;\n if (!id) return;\n viewport.store.update(id, { position });\n if (size) {\n viewport.store.update(id, { size });\n }\n viewport.requestRender();\n }, [viewport, position.x, position.y, size?.w, size?.h]);\n\n if (!portalTarget) return null;\n return createPortal(children, portalTarget);\n}\n","import { useContext } from 'react';\nimport type { Viewport } from '@fieldnotes/core';\nimport { ViewportContext } from '../context';\n\n/** The Viewport from the nearest `<FieldNotesCanvas>`. Throws outside one. */\nexport function useViewport(): Viewport {\n const viewport = useContext(ViewportContext);\n if (!viewport) {\n throw new Error('useViewport must be used inside <FieldNotesCanvas>');\n }\n return viewport;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\n/** `[activeToolName, setTool]` — re-renders on tool switches. */\nexport function useActiveTool(): [toolName: string, setTool: (name: string) => void] {\n const viewport = useViewport();\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.toolManager.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? '', [viewport]);\n\n const toolName = useSyncExternalStore(subscribe, getSnapshot);\n\n const setTool = useCallback((name: string) => viewport.setTool(name), [viewport]);\n\n return [toolName, setTool];\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\nexport interface CameraState {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** Current camera position and zoom level — re-renders whenever the camera moves or zoom changes. */\nexport function useCamera(): CameraState {\n const viewport = useViewport();\n const cachedRef = useRef<CameraState>({ x: 0, y: 0, zoom: 1 });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.camera.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CameraState => {\n const { position, zoom } = viewport.camera;\n const cached = cachedRef.current;\n if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {\n return cached;\n }\n const next = { x: position.x, y: position.y, zoom };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\nfunction noop() {\n /* unsubscribe placeholder */\n}\n\n/**\n * `[options, setOptions]` for the named tool — re-renders when the tool's options change.\n * Returns `null` for options if the tool is not registered or does not expose options.\n */\nexport function useToolOptions<T extends Record<string, unknown>>(\n toolName: string,\n): [options: T | null, setOptions: (partial: Partial<T>) => void] {\n const viewport = useViewport();\n const cachedRef = useRef<T | null>(null);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const tool = viewport.toolManager.getTool(toolName);\n if (!tool?.onOptionsChange) return noop;\n return tool.onOptionsChange(onStoreChange);\n },\n [viewport, toolName],\n );\n\n const getSnapshot = useCallback((): T | null => {\n const tool = viewport.toolManager.getTool(toolName);\n if (!tool?.getOptions) return null;\n const next = tool.getOptions() as T;\n const cached = cachedRef.current;\n if (cached && shallowEqual(cached, next)) return cached;\n cachedRef.current = next;\n return next;\n }, [viewport, toolName]);\n\n const options = useSyncExternalStore(subscribe, getSnapshot);\n\n const setOptions = useCallback(\n (partial: Partial<T>) => {\n const tool = viewport.toolManager.getTool(toolName);\n tool?.setOptions?.(partial as Record<string, unknown>);\n },\n [viewport, toolName],\n );\n\n return [options, setOptions];\n}\n\nfunction shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n const valA = a[key];\n const valB = b[key];\n if (valA === valB) continue;\n if (\n typeof valA === 'object' &&\n valA !== null &&\n typeof valB === 'object' &&\n valB !== null &&\n !Array.isArray(valA) &&\n !Array.isArray(valB)\n ) {\n const objA = valA as Record<string, unknown>;\n const objB = valB as Record<string, unknown>;\n const innerKeys = Object.keys(objA);\n if (\n innerKeys.length === Object.keys(objB).length &&\n innerKeys.every((k) => objA[k] === objB[k])\n ) {\n continue;\n }\n }\n return false;\n }\n return true;\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport type { Layer } from '@fieldnotes/core';\nimport { useViewport } from './use-viewport';\n\ninterface LayersSnapshot {\n layers: Layer[];\n activeLayerId: string;\n}\n\nexport interface UseLayersResult {\n layers: Layer[];\n activeLayerId: string;\n createLayer: (name?: string) => Layer;\n removeLayer: (id: string) => void;\n renameLayer: (id: string, name: string) => void;\n reorderLayer: (id: string, newOrder: number) => void;\n setVisible: (id: string, visible: boolean) => void;\n setLocked: (id: string, locked: boolean) => void;\n setOpacity: (id: string, opacity: number) => void;\n setActiveLayer: (id: string) => void;\n moveElement: (elementId: string, layerId: string) => void;\n}\n\n/**\n * Current layer list and mutation helpers — re-renders whenever a layer is added, removed,\n * reordered, renamed, or its visibility/lock/opacity/active state changes.\n */\nexport function useLayers(): UseLayersResult {\n const viewport = useViewport();\n const cachedRef = useRef<LayersSnapshot>({ layers: [], activeLayerId: '' });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.layerManager.on('change', onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): LayersSnapshot => {\n const layers = viewport.layerManager.getLayers();\n const activeLayerId = viewport.layerManager.activeLayerId;\n const cached = cachedRef.current;\n if (\n cached.activeLayerId === activeLayerId &&\n cached.layers.length === layers.length &&\n cached.layers.every((l, i) => {\n const next = layers[i];\n return (\n next !== undefined &&\n l.id === next.id &&\n l.name === next.name &&\n l.visible === next.visible &&\n l.locked === next.locked &&\n l.order === next.order &&\n l.opacity === next.opacity\n );\n })\n ) {\n return cached;\n }\n const next = { layers, activeLayerId };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n\n const createLayer = useCallback(\n (name?: string) => viewport.layerManager.createLayer(name),\n [viewport],\n );\n\n const removeLayer = useCallback(\n (id: string) => viewport.layerManager.removeLayer(id),\n [viewport],\n );\n\n const renameLayer = useCallback(\n (id: string, name: string) => viewport.layerManager.renameLayer(id, name),\n [viewport],\n );\n\n const reorderLayer = useCallback(\n (id: string, newOrder: number) => viewport.layerManager.reorderLayer(id, newOrder),\n [viewport],\n );\n\n const setVisible = useCallback(\n (id: string, visible: boolean) => viewport.layerManager.setLayerVisible(id, visible),\n [viewport],\n );\n\n const setLocked = useCallback(\n (id: string, locked: boolean) => viewport.layerManager.setLayerLocked(id, locked),\n [viewport],\n );\n\n const setOpacity = useCallback(\n (id: string, opacity: number) => viewport.layerManager.setLayerOpacity(id, opacity),\n [viewport],\n );\n\n const setActiveLayer = useCallback(\n (id: string) => viewport.layerManager.setActiveLayer(id),\n [viewport],\n );\n\n const moveElement = useCallback(\n (elementId: string, layerId: string) =>\n viewport.layerManager.moveElementToLayer(elementId, layerId),\n [viewport],\n );\n\n return {\n layers: snapshot.layers,\n activeLayerId: snapshot.activeLayerId,\n createLayer,\n removeLayer,\n renameLayer,\n reorderLayer,\n setVisible,\n setLocked,\n setOpacity,\n setActiveLayer,\n moveElement,\n };\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport { useViewport } from './use-viewport';\n\ninterface HistorySnapshot {\n canUndo: boolean;\n canRedo: boolean;\n}\n\nexport interface UseHistoryResult {\n canUndo: boolean;\n canRedo: boolean;\n undo: () => void;\n redo: () => void;\n}\n\n/** `{ canUndo, canRedo, undo, redo }` — re-renders when undo/redo availability changes. */\nexport function useHistory(): UseHistoryResult {\n const viewport = useViewport();\n const cachedRef = useRef<HistorySnapshot>({ canUndo: false, canRedo: false });\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.history.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): HistorySnapshot => {\n const canUndo = viewport.history.canUndo;\n const canRedo = viewport.history.canRedo;\n const cached = cachedRef.current;\n if (cached.canUndo === canUndo && cached.canRedo === canRedo) {\n return cached;\n }\n const next = { canUndo, canRedo };\n cachedRef.current = next;\n return next;\n }, [viewport]);\n\n const snapshot = useSyncExternalStore(subscribe, getSnapshot);\n\n const undo = useCallback(() => viewport.undo(), [viewport]);\n const redo = useCallback(() => viewport.redo(), [viewport]);\n\n return {\n canUndo: snapshot.canUndo,\n canRedo: snapshot.canRedo,\n undo,\n redo,\n };\n}\n","import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport type { CanvasElement, ElementType } from '@fieldnotes/core';\nimport { useViewport } from './use-viewport';\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));\n }\n if (\n typeof a === 'object' &&\n a !== null &&\n typeof b === 'object' &&\n b !== null &&\n !Array.isArray(a) &&\n !Array.isArray(b)\n ) {\n const ka = Object.keys(a);\n const kb = Object.keys(b);\n return (\n ka.length === kb.length &&\n ka.every(\n (k) =>\n k in (b as Record<string, unknown>) &&\n Object.is((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),\n )\n );\n }\n return false;\n}\n\n/** All elements; re-renders on every store mutation (use a selector for hot paths). */\nexport function useElements(): CanvasElement[];\n/** Elements of one type; re-renders on every store mutation. */\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\n/**\n * Derived value; re-renders only when the selected value changes (per `isEqual`,\n * default: one-level shallow compare — arrays by index, plain objects by key, primitives by\n * Object.is). Keep the selector referentially stable (useCallback or module scope).\n *\n * Supply `isEqual` for deeper structures. Note: selectors returning nested fresh objects\n * (e.g. `els => els.map(e => ({ ...e.position }))`) still need a custom comparator because\n * the default only compares one level deep.\n */\nexport function useElements<R>(\n selector: (elements: CanvasElement[]) => R,\n isEqual?: (a: R, b: R) => boolean,\n): R;\nexport function useElements<R>(\n arg?: ElementType | ((elements: CanvasElement[]) => R),\n isEqual?: (a: R, b: R) => boolean,\n): CanvasElement[] | R {\n const viewport = useViewport();\n const cachedRef = useRef<CanvasElement[]>([]);\n const selectedRef = useRef<{ value: R } | null>(null);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CanvasElement[] | R => {\n if (typeof arg === 'function') {\n const next = arg(viewport.store.getAll());\n const prev = selectedRef.current;\n const equal: (a: R, b: R) => boolean = isEqual ?? (shallowEqual as (a: R, b: R) => boolean);\n if (prev !== null && equal(prev.value, next)) {\n return prev.value;\n }\n selectedRef.current = { value: next };\n return next;\n }\n\n const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();\n const cached = cachedRef.current;\n if (cached.length === next.length && cached.every((el, i) => el === next[i])) {\n return cached;\n }\n cachedRef.current = next;\n return next;\n }, [viewport, arg, isEqual]);\n\n return useSyncExternalStore(subscribe, getSnapshot);\n}\n"],"mappings":"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","names":["FieldNotesCanvas","useEffect","useRef","useState","useState","useRef","useEffect","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","next","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","useRef","useCallback","useSyncExternalStore","useCallback","useRef","useSyncExternalStore","shallowEqual","useRef","useCallback","next","useSyncExternalStore"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/react",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "React wrapper for Field Notes canvas SDK",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -50,7 +50,7 @@
50
50
  "react-dom": "^19.2.4",
51
51
  "tsup": "^8.5.1",
52
52
  "vitest": "^4.1.0",
53
- "@fieldnotes/core": "0.19.0"
53
+ "@fieldnotes/core": "0.21.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsup",