@fieldnotes/react 0.4.2 → 0.6.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 +264 -151
- package/dist/index.cjs +118 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +116 -13
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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,276 @@ 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
|
-
|
|
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.
|
|
62
124
|
|
|
63
|
-
|
|
125
|
+
## Undo / Redo
|
|
64
126
|
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
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.
|
|
176
|
+
|
|
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:
|
|
122
178
|
|
|
123
|
-
|
|
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
|
+
```
|
|
124
185
|
|
|
125
|
-
|
|
186
|
+
## Selection & Styling
|
|
187
|
+
|
|
188
|
+
### `useSelection`
|
|
189
|
+
|
|
190
|
+
Returns the current selection as a reactive, referentially-stable `string[]` of element IDs. Re-renders only when the selection changes.
|
|
126
191
|
|
|
127
192
|
```tsx
|
|
128
|
-
import {
|
|
129
|
-
|
|
130
|
-
function LayersPanel() {
|
|
131
|
-
const {
|
|
132
|
-
layers,
|
|
133
|
-
activeLayerId,
|
|
134
|
-
createLayer,
|
|
135
|
-
removeLayer,
|
|
136
|
-
setVisible,
|
|
137
|
-
setLocked,
|
|
138
|
-
setOpacity,
|
|
139
|
-
setActiveLayer,
|
|
140
|
-
} = useLayers();
|
|
193
|
+
import { useSelection } from '@fieldnotes/react';
|
|
141
194
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
);
|
|
195
|
+
function SelectionBadge() {
|
|
196
|
+
const ids = useSelection();
|
|
197
|
+
return <span>{ids.length} selected</span>;
|
|
168
198
|
}
|
|
169
199
|
```
|
|
170
200
|
|
|
171
|
-
|
|
201
|
+
### `useSelectionStyle`
|
|
172
202
|
|
|
173
|
-
|
|
203
|
+
Returns `[style, applyStyle]` — the shared style of the current selection and a stable callback to apply a style patch to it.
|
|
174
204
|
|
|
175
|
-
|
|
205
|
+
- `style` is an `ElementStyle` (`{ color?, fillColor?, strokeWidth?, opacity?, fontSize? }`) containing only properties that are identical across all selected elements. Properties that differ are omitted. `style` is `null` when nothing is selected.
|
|
206
|
+
- `applyStyle(patch)` applies the patch to the current selection in a single undo step.
|
|
176
207
|
|
|
177
208
|
```tsx
|
|
178
|
-
import {
|
|
209
|
+
import { useSelectionStyle } from '@fieldnotes/react';
|
|
179
210
|
|
|
180
|
-
function
|
|
181
|
-
const
|
|
211
|
+
function StyleToolbar() {
|
|
212
|
+
const [style, applyStyle] = useSelectionStyle();
|
|
182
213
|
|
|
183
214
|
return (
|
|
184
215
|
<div>
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
<input
|
|
217
|
+
type="color"
|
|
218
|
+
value={style?.color ?? '#000000'}
|
|
219
|
+
disabled={!style}
|
|
220
|
+
onChange={(e) => applyStyle({ color: e.target.value })}
|
|
221
|
+
/>
|
|
222
|
+
<input
|
|
223
|
+
type="range"
|
|
224
|
+
min={1}
|
|
225
|
+
max={20}
|
|
226
|
+
value={style?.strokeWidth ?? 2}
|
|
227
|
+
disabled={!style}
|
|
228
|
+
onChange={(e) => applyStyle({ strokeWidth: Number(e.target.value) })}
|
|
229
|
+
/>
|
|
191
230
|
</div>
|
|
192
231
|
);
|
|
193
232
|
}
|
|
194
233
|
```
|
|
195
234
|
|
|
196
|
-
|
|
235
|
+
See [core README](../core/README.md#styling-the-selection) for the full `ElementStyle` mapping table and the underlying `Viewport` methods.
|
|
197
236
|
|
|
198
|
-
|
|
237
|
+
## Save / Load
|
|
238
|
+
|
|
239
|
+
Access the viewport imperatively for export and import:
|
|
199
240
|
|
|
200
241
|
```tsx
|
|
201
|
-
import {
|
|
242
|
+
import { useViewport } from '@fieldnotes/react';
|
|
202
243
|
|
|
203
|
-
function
|
|
204
|
-
const
|
|
205
|
-
|
|
244
|
+
export function SaveControls() {
|
|
245
|
+
const viewport = useViewport();
|
|
246
|
+
|
|
247
|
+
const save = () => {
|
|
248
|
+
localStorage.setItem('fieldnotes-example', viewport.exportJSON());
|
|
249
|
+
};
|
|
250
|
+
const load = () => {
|
|
251
|
+
const json = localStorage.getItem('fieldnotes-example');
|
|
252
|
+
if (json) viewport.loadJSON(json);
|
|
253
|
+
};
|
|
206
254
|
|
|
207
255
|
return (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
<div>
|
|
257
|
+
<button onClick={save}>Save</button>
|
|
258
|
+
<button onClick={load}>Load</button>
|
|
259
|
+
</div>
|
|
211
260
|
);
|
|
212
261
|
}
|
|
213
262
|
```
|
|
214
263
|
|
|
215
|
-
|
|
264
|
+
For periodic auto-saving, use `AutoSave` from `@fieldnotes/core` — it debounces writes to `localStorage` and subscribes to the store and camera automatically:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { AutoSave } from '@fieldnotes/core';
|
|
268
|
+
|
|
269
|
+
// in onReady or a useEffect after useViewport():
|
|
270
|
+
const autoSave = new AutoSave(viewport.store, viewport.camera, {
|
|
271
|
+
key: 'my-board',
|
|
272
|
+
layerManager: viewport.layerManager,
|
|
273
|
+
});
|
|
274
|
+
autoSave.start();
|
|
275
|
+
// call autoSave.stop() on cleanup
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Pass `layerManager` — without it, saved boards lose their layer structure.
|
|
279
|
+
|
|
280
|
+
## Custom Tools
|
|
216
281
|
|
|
217
|
-
|
|
282
|
+
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:
|
|
218
283
|
|
|
219
|
-
|
|
284
|
+
```tsx
|
|
285
|
+
import type { Tool, ToolContext, PointerState } from '@fieldnotes/core';
|
|
286
|
+
import { createNote } from '@fieldnotes/core';
|
|
287
|
+
|
|
288
|
+
export class StampTool implements Tool {
|
|
289
|
+
readonly name = 'stamp';
|
|
290
|
+
|
|
291
|
+
onPointerDown(state: PointerState, ctx: ToolContext): void {
|
|
292
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
293
|
+
ctx.store.add(
|
|
294
|
+
createNote({
|
|
295
|
+
position: { x: world.x - 50, y: world.y - 25 },
|
|
296
|
+
size: { w: 100, h: 50 },
|
|
297
|
+
text: 'stamp',
|
|
298
|
+
layerId: ctx.activeLayerId ?? '',
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
ctx.requestRender();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
onPointerMove(_state: PointerState, _ctx: ToolContext): void {}
|
|
305
|
+
onPointerUp(_state: PointerState, _ctx: ToolContext): void {}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Register alongside the built-in tools:
|
|
220
310
|
|
|
221
311
|
```tsx
|
|
222
|
-
import {
|
|
312
|
+
import { HandTool, SelectTool } from '@fieldnotes/core';
|
|
313
|
+
import { StampTool } from './StampTool';
|
|
223
314
|
|
|
224
|
-
|
|
225
|
-
|
|
315
|
+
// Hoisted — stable reference across renders
|
|
316
|
+
const TOOLS = [new HandTool(), new SelectTool(), new StampTool()];
|
|
226
317
|
|
|
318
|
+
function App() {
|
|
319
|
+
const [tool, setTool] = useState('select');
|
|
227
320
|
return (
|
|
228
|
-
<
|
|
229
|
-
{
|
|
230
|
-
</
|
|
321
|
+
<FieldNotesCanvas tools={TOOLS} tool={tool} onToolChange={setTool} style={...}>
|
|
322
|
+
<Toolbar tool={tool} onSelect={setTool} />
|
|
323
|
+
</FieldNotesCanvas>
|
|
231
324
|
);
|
|
232
325
|
}
|
|
233
326
|
```
|
|
234
327
|
|
|
235
|
-
|
|
328
|
+
## Events
|
|
236
329
|
|
|
237
|
-
|
|
330
|
+
### `onReady`
|
|
331
|
+
|
|
332
|
+
Fires once after the `Viewport` is created and tools are registered. Use it for imperative setup that can't wait for a child hook:
|
|
238
333
|
|
|
239
334
|
```tsx
|
|
240
|
-
|
|
335
|
+
<FieldNotesCanvas
|
|
336
|
+
tools={TOOLS}
|
|
337
|
+
onReady={(viewport) => {
|
|
338
|
+
const saved = localStorage.getItem('board');
|
|
339
|
+
if (saved) viewport.loadJSON(saved);
|
|
340
|
+
}}
|
|
341
|
+
style={{ width: '100%', height: '100%' }}
|
|
342
|
+
/>
|
|
343
|
+
```
|
|
241
344
|
|
|
242
|
-
|
|
243
|
-
const viewport = useViewport();
|
|
345
|
+
### `onToolChange`
|
|
244
346
|
|
|
245
|
-
|
|
246
|
-
|
|
347
|
+
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:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
const [tool, setTool] = useState('select');
|
|
351
|
+
|
|
352
|
+
<FieldNotesCanvas tool={tool} onToolChange={setTool} tools={TOOLS} style={...} />
|
|
247
353
|
```
|
|
248
354
|
|
|
249
|
-
|
|
355
|
+
### `options.onImageError`
|
|
250
356
|
|
|
251
|
-
|
|
357
|
+
Called when an image element fails to load. Receives `{ src: string; elementIds: string[] }` — the source URL and all element IDs that reference it:
|
|
252
358
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
359
|
+
```tsx
|
|
360
|
+
<FieldNotesCanvas
|
|
361
|
+
tools={TOOLS}
|
|
362
|
+
options={{
|
|
363
|
+
onImageError: ({ src, elementIds }) => {
|
|
364
|
+
console.warn(`Image failed: ${src}`, elementIds);
|
|
365
|
+
},
|
|
366
|
+
}}
|
|
367
|
+
style={{ width: '100%', height: '100%' }}
|
|
368
|
+
/>
|
|
369
|
+
```
|
|
263
370
|
|
|
264
|
-
###
|
|
371
|
+
### `options.onDrop`
|
|
265
372
|
|
|
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 |
|
|
373
|
+
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
374
|
|
|
272
|
-
|
|
375
|
+
```tsx
|
|
376
|
+
<FieldNotesCanvas
|
|
377
|
+
tools={TOOLS}
|
|
378
|
+
options={{
|
|
379
|
+
onDrop: (event, worldPosition) => {
|
|
380
|
+
const url = event.dataTransfer?.getData('text/uri-list');
|
|
381
|
+
if (url) {
|
|
382
|
+
// add your own image element at worldPosition
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
}}
|
|
386
|
+
style={{ width: '100%', height: '100%' }}
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
273
389
|
|
|
274
|
-
##
|
|
390
|
+
## Escape Hatch
|
|
275
391
|
|
|
276
|
-
|
|
392
|
+
`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
393
|
|
|
278
394
|
```tsx
|
|
279
395
|
import { useRef } from 'react';
|
|
@@ -282,20 +398,17 @@ import { FieldNotesCanvas, type FieldNotesCanvasRef } from '@fieldnotes/react';
|
|
|
282
398
|
function App() {
|
|
283
399
|
const canvasRef = useRef<FieldNotesCanvasRef>(null);
|
|
284
400
|
|
|
285
|
-
const exportState = () => {
|
|
286
|
-
const json = canvasRef.current?.viewport?.exportJSON();
|
|
287
|
-
console.log(json);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
401
|
return (
|
|
291
402
|
<>
|
|
292
403
|
<FieldNotesCanvas ref={canvasRef} style={{ width: '100vw', height: '100vh' }} />
|
|
293
|
-
<button onClick={
|
|
404
|
+
<button onClick={() => canvasRef.current?.viewport?.fitToContent()}>Fit to content</button>
|
|
294
405
|
</>
|
|
295
406
|
);
|
|
296
407
|
}
|
|
297
408
|
```
|
|
298
409
|
|
|
410
|
+
See the [core README](../core/README.md) for the full `Viewport` API — `exportJSON`/`loadJSON`, `fitToContent`, `shortcuts.rebind`, `setSnapToGrid`, and more.
|
|
411
|
+
|
|
299
412
|
## Versioning
|
|
300
413
|
|
|
301
414
|
`@fieldnotes/core` and `@fieldnotes/react` are versioned independently. The react
|