@fieldnotes/react 0.4.1 → 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/LICENSE +21 -21
- package/README.md +223 -160
- package/dist/index.cjs +69 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +69 -13
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Irakli Iremashvili
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Irakli Iremashvili
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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 {
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
## Undo / Redo
|
|
66
126
|
|
|
67
|
-
|
|
127
|
+
`useHistory` returns reactive undo/redo state and action callbacks:
|
|
68
128
|
|
|
69
129
|
```tsx
|
|
70
|
-
import {
|
|
130
|
+
import { useHistory } from '@fieldnotes/react';
|
|
71
131
|
|
|
72
|
-
function
|
|
73
|
-
const
|
|
132
|
+
function UndoRedo() {
|
|
133
|
+
const { canUndo, canRedo, undo, redo } = useHistory();
|
|
74
134
|
|
|
75
135
|
return (
|
|
76
136
|
<div>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
148
|
+
## Why Is My Sidebar Re-rendering 60×/s?
|
|
86
149
|
|
|
87
|
-
|
|
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 {
|
|
91
|
-
import
|
|
153
|
+
import { useCallback } from 'react';
|
|
154
|
+
import { useElements } from '@fieldnotes/react';
|
|
155
|
+
import type { CanvasElement } from '@fieldnotes/core';
|
|
92
156
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
const
|
|
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
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
175
|
+
Alternatively, hoist expensive rendering into a memoized child and pass elements as props — React's `memo` then does the comparison.
|
|
122
176
|
|
|
123
|
-
|
|
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
|
-
|
|
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 {
|
|
129
|
-
|
|
130
|
-
function
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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={
|
|
145
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
Pass `layerManager` — without it, saved boards lose their layer structure.
|
|
176
228
|
|
|
177
|
-
|
|
178
|
-
import { useHistory } from '@fieldnotes/react';
|
|
229
|
+
## Custom Tools
|
|
179
230
|
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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 {
|
|
261
|
+
import { HandTool, SelectTool } from '@fieldnotes/core';
|
|
262
|
+
import { StampTool } from './StampTool';
|
|
202
263
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
<
|
|
209
|
-
{
|
|
210
|
-
</
|
|
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
|
-
|
|
277
|
+
## Events
|
|
216
278
|
|
|
217
|
-
### `
|
|
279
|
+
### `onReady`
|
|
218
280
|
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
### `
|
|
294
|
+
### `onToolChange`
|
|
236
295
|
|
|
237
|
-
|
|
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
|
-
|
|
299
|
+
const [tool, setTool] = useState('select');
|
|
241
300
|
|
|
242
|
-
|
|
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
|
-
|
|
304
|
+
### `options.onImageError`
|
|
250
305
|
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
###
|
|
320
|
+
### `options.onDrop`
|
|
265
321
|
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
339
|
+
## Escape Hatch
|
|
275
340
|
|
|
276
|
-
|
|
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,25 +347,23 @@ 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={
|
|
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
|
|
302
364
|
package's `peerDependencies` declare the compatible core range. Pre-1.0, minor
|
|
303
|
-
versions may contain breaking changes.
|
|
365
|
+
versions may contain breaking changes. The core peer range is bounded at the next major rather than per-minor; if a core minor
|
|
366
|
+
ever breaks the wrapper, a coordinated react release raises the lower bound.
|
|
304
367
|
|
|
305
368
|
## License
|
|
306
369
|
|