@fieldnotes/react 0.2.1 → 0.4.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 +301 -190
- package/dist/index.cjs +199 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -3
- package/dist/index.d.ts +33 -3
- package/dist/index.js +195 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -1,190 +1,301 @@
|
|
|
1
|
-
# @fieldnotes/react
|
|
2
|
-
|
|
3
|
-
React bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes) infinite canvas SDK. Embed React components directly onto an infinite, pannable, zoomable canvas.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @fieldnotes/core @fieldnotes/react
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Requires React 18+.
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```tsx
|
|
16
|
-
import { FieldNotesCanvas } from '@fieldnotes/react';
|
|
17
|
-
import { HandTool, SelectTool, PencilTool } from '@fieldnotes/core';
|
|
18
|
-
|
|
19
|
-
function App() {
|
|
20
|
-
return (
|
|
21
|
-
<FieldNotesCanvas
|
|
22
|
-
tools={[new HandTool(), new SelectTool(), new PencilTool()]}
|
|
23
|
-
defaultTool="select"
|
|
24
|
-
style={{ width: '100vw', height: '100vh' }}
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Your container needs a defined size — the canvas fills it.
|
|
31
|
-
|
|
32
|
-
## Embedding React Components
|
|
33
|
-
|
|
34
|
-
The main feature — render any React component as a canvas element that pans, zooms, and resizes with the canvas:
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { FieldNotesCanvas, CanvasElement } from '@fieldnotes/react';
|
|
38
|
-
import { SelectTool } from '@fieldnotes/core';
|
|
39
|
-
|
|
40
|
-
function App() {
|
|
41
|
-
return (
|
|
42
|
-
<FieldNotesCanvas
|
|
43
|
-
tools={[new SelectTool()]}
|
|
44
|
-
defaultTool="select"
|
|
45
|
-
style={{ width: '100vw', height: '100vh' }}
|
|
46
|
-
>
|
|
47
|
-
<CanvasElement position={{ x: 100, y: 200 }} size={{ w: 300, h: 200 }}>
|
|
48
|
-
<MyCard />
|
|
49
|
-
</CanvasElement>
|
|
50
|
-
|
|
51
|
-
<CanvasElement position={{ x: 500, y: 100 }}>
|
|
52
|
-
<button onClick={() => console.log('clicked!')}>Interactive button on the canvas</button>
|
|
53
|
-
</CanvasElement>
|
|
54
|
-
</FieldNotesCanvas>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
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
|
-
|
|
61
|
-
## Hooks
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
1
|
+
# @fieldnotes/react
|
|
2
|
+
|
|
3
|
+
React bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes) infinite canvas SDK. Embed React components directly onto an infinite, pannable, zoomable canvas.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fieldnotes/core @fieldnotes/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires React 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { FieldNotesCanvas } from '@fieldnotes/react';
|
|
17
|
+
import { HandTool, SelectTool, PencilTool } from '@fieldnotes/core';
|
|
18
|
+
|
|
19
|
+
function App() {
|
|
20
|
+
return (
|
|
21
|
+
<FieldNotesCanvas
|
|
22
|
+
tools={[new HandTool(), new SelectTool(), new PencilTool()]}
|
|
23
|
+
defaultTool="select"
|
|
24
|
+
style={{ width: '100vw', height: '100vh' }}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Your container needs a defined size — the canvas fills it.
|
|
31
|
+
|
|
32
|
+
## Embedding React Components
|
|
33
|
+
|
|
34
|
+
The main feature — render any React component as a canvas element that pans, zooms, and resizes with the canvas:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { FieldNotesCanvas, CanvasElement } from '@fieldnotes/react';
|
|
38
|
+
import { SelectTool } from '@fieldnotes/core';
|
|
39
|
+
|
|
40
|
+
function App() {
|
|
41
|
+
return (
|
|
42
|
+
<FieldNotesCanvas
|
|
43
|
+
tools={[new SelectTool()]}
|
|
44
|
+
defaultTool="select"
|
|
45
|
+
style={{ width: '100vw', height: '100vh' }}
|
|
46
|
+
>
|
|
47
|
+
<CanvasElement position={{ x: 100, y: 200 }} size={{ w: 300, h: 200 }}>
|
|
48
|
+
<MyCard />
|
|
49
|
+
</CanvasElement>
|
|
50
|
+
|
|
51
|
+
<CanvasElement position={{ x: 500, y: 100 }}>
|
|
52
|
+
<button onClick={() => console.log('clicked!')}>Interactive button on the canvas</button>
|
|
53
|
+
</CanvasElement>
|
|
54
|
+
</FieldNotesCanvas>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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
|
+
|
|
61
|
+
## Hooks
|
|
62
|
+
|
|
63
|
+
All hooks must be used inside `<FieldNotesCanvas>`.
|
|
64
|
+
|
|
65
|
+
### `useActiveTool()`
|
|
66
|
+
|
|
67
|
+
Reactive tool name + setter — re-renders when the active tool changes:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { useActiveTool } from '@fieldnotes/react';
|
|
71
|
+
|
|
72
|
+
function Toolbar() {
|
|
73
|
+
const [tool, setTool] = useActiveTool();
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<span>Current: {tool}</span>
|
|
78
|
+
<button onClick={() => setTool('pencil')}>Pencil</button>
|
|
79
|
+
<button onClick={() => setTool('select')}>Select</button>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `useToolOptions(toolName)`
|
|
86
|
+
|
|
87
|
+
Reactive tool options with two-way sync — read and write tool configuration:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { useActiveTool, useToolOptions } from '@fieldnotes/react';
|
|
91
|
+
import type { PencilToolOptions } from '@fieldnotes/core';
|
|
92
|
+
|
|
93
|
+
function PencilSettings() {
|
|
94
|
+
const [tool, setTool] = useActiveTool();
|
|
95
|
+
const [opts, setOpts] = useToolOptions<PencilToolOptions>('pencil');
|
|
96
|
+
|
|
97
|
+
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
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Returns `[null, noop]` for tools that don't support options (e.g., `HandTool`).
|
|
122
|
+
|
|
123
|
+
### `useLayers()`
|
|
124
|
+
|
|
125
|
+
Full layer management — reactive layer list with action callbacks:
|
|
126
|
+
|
|
127
|
+
```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();
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<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
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Also exposes: `renameLayer`, `reorderLayer`, `moveElement`.
|
|
172
|
+
|
|
173
|
+
### `useHistory()`
|
|
174
|
+
|
|
175
|
+
Reactive undo/redo state:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { useHistory } from '@fieldnotes/react';
|
|
179
|
+
|
|
180
|
+
function UndoRedo() {
|
|
181
|
+
const { canUndo, canRedo, undo, redo } = useHistory();
|
|
182
|
+
|
|
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
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `useElements(type?)`
|
|
197
|
+
|
|
198
|
+
Reactive element list — re-renders when elements are added, removed, or updated:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useElements } from '@fieldnotes/react';
|
|
202
|
+
|
|
203
|
+
function ElementCount() {
|
|
204
|
+
const elements = useElements();
|
|
205
|
+
const notes = useElements('note');
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<span>
|
|
209
|
+
{notes.length} notes / {elements.length} total
|
|
210
|
+
</span>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Pass an element type (`'note'`, `'stroke'`, `'arrow'`, etc.) to filter.
|
|
216
|
+
|
|
217
|
+
### `useCamera()`
|
|
218
|
+
|
|
219
|
+
Reactive camera state (position + zoom) — re-renders on pan/zoom:
|
|
220
|
+
|
|
221
|
+
```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
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `useViewport()`
|
|
236
|
+
|
|
237
|
+
Access the core `Viewport` instance for imperative operations not covered by the hooks above:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { useViewport } from '@fieldnotes/react';
|
|
241
|
+
|
|
242
|
+
function ExportButton() {
|
|
243
|
+
const viewport = useViewport();
|
|
244
|
+
|
|
245
|
+
return <button onClick={() => viewport.exportImage()}>Export PNG</button>;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Component API
|
|
250
|
+
|
|
251
|
+
### `<FieldNotesCanvas>`
|
|
252
|
+
|
|
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 }` |
|
|
263
|
+
|
|
264
|
+
### `<CanvasElement>`
|
|
265
|
+
|
|
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 |
|
|
271
|
+
|
|
272
|
+
Position and size updates are reactive — change the props and the element moves/resizes on the canvas.
|
|
273
|
+
|
|
274
|
+
## Accessing the Viewport Directly
|
|
275
|
+
|
|
276
|
+
For advanced use cases, use a ref:
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
import { useRef } from 'react';
|
|
280
|
+
import { FieldNotesCanvas, type FieldNotesCanvasRef } from '@fieldnotes/react';
|
|
281
|
+
|
|
282
|
+
function App() {
|
|
283
|
+
const canvasRef = useRef<FieldNotesCanvasRef>(null);
|
|
284
|
+
|
|
285
|
+
const exportState = () => {
|
|
286
|
+
const json = canvasRef.current?.viewport?.exportJSON();
|
|
287
|
+
console.log(json);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<>
|
|
292
|
+
<FieldNotesCanvas ref={canvasRef} style={{ width: '100vw', height: '100vh' }} />
|
|
293
|
+
<button onClick={exportState}>Export</button>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,10 @@ __export(index_exports, {
|
|
|
25
25
|
ViewportContext: () => ViewportContext,
|
|
26
26
|
useActiveTool: () => useActiveTool,
|
|
27
27
|
useCamera: () => useCamera,
|
|
28
|
+
useElements: () => useElements,
|
|
29
|
+
useHistory: () => useHistory,
|
|
30
|
+
useLayers: () => useLayers,
|
|
31
|
+
useToolOptions: () => useToolOptions,
|
|
28
32
|
useViewport: () => useViewport
|
|
29
33
|
});
|
|
30
34
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -71,7 +75,7 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
|
|
|
71
75
|
var import_react4 = require("react");
|
|
72
76
|
var import_react_dom = require("react-dom");
|
|
73
77
|
|
|
74
|
-
// src/use-viewport.ts
|
|
78
|
+
// src/hooks/use-viewport.ts
|
|
75
79
|
var import_react3 = require("react");
|
|
76
80
|
function useViewport() {
|
|
77
81
|
const viewport = (0, import_react3.useContext)(ViewportContext);
|
|
@@ -118,7 +122,7 @@ function CanvasElement({ position, size, children }) {
|
|
|
118
122
|
return (0, import_react_dom.createPortal)(children, portalTarget);
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
// src/use-active-tool.ts
|
|
125
|
+
// src/hooks/use-active-tool.ts
|
|
122
126
|
var import_react5 = require("react");
|
|
123
127
|
function useActiveTool() {
|
|
124
128
|
const viewport = useViewport();
|
|
@@ -127,10 +131,15 @@ function useActiveTool() {
|
|
|
127
131
|
[viewport]
|
|
128
132
|
);
|
|
129
133
|
const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
|
|
130
|
-
|
|
134
|
+
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
|
+
);
|
|
139
|
+
return [toolName, setTool];
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
// src/use-camera.ts
|
|
142
|
+
// src/hooks/use-camera.ts
|
|
134
143
|
var import_react6 = require("react");
|
|
135
144
|
function useCamera() {
|
|
136
145
|
const viewport = useViewport();
|
|
@@ -151,6 +160,188 @@ function useCamera() {
|
|
|
151
160
|
}, [viewport]);
|
|
152
161
|
return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot);
|
|
153
162
|
}
|
|
163
|
+
|
|
164
|
+
// src/hooks/use-tool-options.ts
|
|
165
|
+
var import_react7 = require("react");
|
|
166
|
+
function noop() {
|
|
167
|
+
}
|
|
168
|
+
function useToolOptions(toolName) {
|
|
169
|
+
const viewport = useViewport();
|
|
170
|
+
const cachedRef = (0, import_react7.useRef)(null);
|
|
171
|
+
const subscribe = (0, import_react7.useCallback)(
|
|
172
|
+
(onStoreChange) => {
|
|
173
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
174
|
+
if (!tool?.onOptionsChange) return noop;
|
|
175
|
+
return tool.onOptionsChange(onStoreChange);
|
|
176
|
+
},
|
|
177
|
+
[viewport, toolName]
|
|
178
|
+
);
|
|
179
|
+
const getSnapshot = (0, import_react7.useCallback)(() => {
|
|
180
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
181
|
+
if (!tool?.getOptions) return null;
|
|
182
|
+
const next = tool.getOptions();
|
|
183
|
+
const cached = cachedRef.current;
|
|
184
|
+
if (cached && shallowEqual(cached, next)) return cached;
|
|
185
|
+
cachedRef.current = next;
|
|
186
|
+
return next;
|
|
187
|
+
}, [viewport, toolName]);
|
|
188
|
+
const options = (0, import_react7.useSyncExternalStore)(subscribe, getSnapshot);
|
|
189
|
+
const setOptions = (0, import_react7.useCallback)(
|
|
190
|
+
(partial) => {
|
|
191
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
192
|
+
tool?.setOptions?.(partial);
|
|
193
|
+
},
|
|
194
|
+
[viewport, toolName]
|
|
195
|
+
);
|
|
196
|
+
return [options, setOptions];
|
|
197
|
+
}
|
|
198
|
+
function shallowEqual(a, b) {
|
|
199
|
+
const keysA = Object.keys(a);
|
|
200
|
+
const keysB = Object.keys(b);
|
|
201
|
+
if (keysA.length !== keysB.length) return false;
|
|
202
|
+
for (const key of keysA) {
|
|
203
|
+
const valA = a[key];
|
|
204
|
+
const valB = b[key];
|
|
205
|
+
if (valA === valB) continue;
|
|
206
|
+
if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
|
|
207
|
+
const objA = valA;
|
|
208
|
+
const objB = valB;
|
|
209
|
+
const innerKeys = Object.keys(objA);
|
|
210
|
+
if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/hooks/use-layers.ts
|
|
220
|
+
var import_react8 = require("react");
|
|
221
|
+
function useLayers() {
|
|
222
|
+
const viewport = useViewport();
|
|
223
|
+
const cachedRef = (0, import_react8.useRef)({ layers: [], activeLayerId: "" });
|
|
224
|
+
const subscribe = (0, import_react8.useCallback)(
|
|
225
|
+
(onStoreChange) => viewport.layerManager.on("change", onStoreChange),
|
|
226
|
+
[viewport]
|
|
227
|
+
);
|
|
228
|
+
const getSnapshot = (0, import_react8.useCallback)(() => {
|
|
229
|
+
const layers = viewport.layerManager.getLayers();
|
|
230
|
+
const activeLayerId = viewport.layerManager.activeLayerId;
|
|
231
|
+
const cached = cachedRef.current;
|
|
232
|
+
if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
|
|
233
|
+
const next2 = layers[i];
|
|
234
|
+
return next2 !== void 0 && l.id === next2.id && l.name === next2.name && l.visible === next2.visible && l.locked === next2.locked && l.order === next2.order && l.opacity === next2.opacity;
|
|
235
|
+
})) {
|
|
236
|
+
return cached;
|
|
237
|
+
}
|
|
238
|
+
const next = { layers, activeLayerId };
|
|
239
|
+
cachedRef.current = next;
|
|
240
|
+
return next;
|
|
241
|
+
}, [viewport]);
|
|
242
|
+
const snapshot = (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot);
|
|
243
|
+
const createLayer = (0, import_react8.useCallback)(
|
|
244
|
+
(name) => viewport.layerManager.createLayer(name),
|
|
245
|
+
[viewport]
|
|
246
|
+
);
|
|
247
|
+
const removeLayer = (0, import_react8.useCallback)(
|
|
248
|
+
(id) => viewport.layerManager.removeLayer(id),
|
|
249
|
+
[viewport]
|
|
250
|
+
);
|
|
251
|
+
const renameLayer = (0, import_react8.useCallback)(
|
|
252
|
+
(id, name) => viewport.layerManager.renameLayer(id, name),
|
|
253
|
+
[viewport]
|
|
254
|
+
);
|
|
255
|
+
const reorderLayer = (0, import_react8.useCallback)(
|
|
256
|
+
(id, newOrder) => viewport.layerManager.reorderLayer(id, newOrder),
|
|
257
|
+
[viewport]
|
|
258
|
+
);
|
|
259
|
+
const setVisible = (0, import_react8.useCallback)(
|
|
260
|
+
(id, visible) => viewport.layerManager.setLayerVisible(id, visible),
|
|
261
|
+
[viewport]
|
|
262
|
+
);
|
|
263
|
+
const setLocked = (0, import_react8.useCallback)(
|
|
264
|
+
(id, locked) => viewport.layerManager.setLayerLocked(id, locked),
|
|
265
|
+
[viewport]
|
|
266
|
+
);
|
|
267
|
+
const setOpacity = (0, import_react8.useCallback)(
|
|
268
|
+
(id, opacity) => viewport.layerManager.setLayerOpacity(id, opacity),
|
|
269
|
+
[viewport]
|
|
270
|
+
);
|
|
271
|
+
const setActiveLayer = (0, import_react8.useCallback)(
|
|
272
|
+
(id) => viewport.layerManager.setActiveLayer(id),
|
|
273
|
+
[viewport]
|
|
274
|
+
);
|
|
275
|
+
const moveElement = (0, import_react8.useCallback)(
|
|
276
|
+
(elementId, layerId) => viewport.layerManager.moveElementToLayer(elementId, layerId),
|
|
277
|
+
[viewport]
|
|
278
|
+
);
|
|
279
|
+
return {
|
|
280
|
+
layers: snapshot.layers,
|
|
281
|
+
activeLayerId: snapshot.activeLayerId,
|
|
282
|
+
createLayer,
|
|
283
|
+
removeLayer,
|
|
284
|
+
renameLayer,
|
|
285
|
+
reorderLayer,
|
|
286
|
+
setVisible,
|
|
287
|
+
setLocked,
|
|
288
|
+
setOpacity,
|
|
289
|
+
setActiveLayer,
|
|
290
|
+
moveElement
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/hooks/use-history.ts
|
|
295
|
+
var import_react9 = require("react");
|
|
296
|
+
function useHistory() {
|
|
297
|
+
const viewport = useViewport();
|
|
298
|
+
const cachedRef = (0, import_react9.useRef)({ canUndo: false, canRedo: false });
|
|
299
|
+
const subscribe = (0, import_react9.useCallback)(
|
|
300
|
+
(onStoreChange) => viewport.history.onChange(onStoreChange),
|
|
301
|
+
[viewport]
|
|
302
|
+
);
|
|
303
|
+
const getSnapshot = (0, import_react9.useCallback)(() => {
|
|
304
|
+
const canUndo = viewport.history.canUndo;
|
|
305
|
+
const canRedo = viewport.history.canRedo;
|
|
306
|
+
const cached = cachedRef.current;
|
|
307
|
+
if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
|
|
308
|
+
return cached;
|
|
309
|
+
}
|
|
310
|
+
const next = { canUndo, canRedo };
|
|
311
|
+
cachedRef.current = next;
|
|
312
|
+
return next;
|
|
313
|
+
}, [viewport]);
|
|
314
|
+
const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot);
|
|
315
|
+
const undo = (0, import_react9.useCallback)(() => viewport.undo(), [viewport]);
|
|
316
|
+
const redo = (0, import_react9.useCallback)(() => viewport.redo(), [viewport]);
|
|
317
|
+
return {
|
|
318
|
+
canUndo: snapshot.canUndo,
|
|
319
|
+
canRedo: snapshot.canRedo,
|
|
320
|
+
undo,
|
|
321
|
+
redo
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/hooks/use-elements.ts
|
|
326
|
+
var import_react10 = require("react");
|
|
327
|
+
function useElements(type) {
|
|
328
|
+
const viewport = useViewport();
|
|
329
|
+
const cachedRef = (0, import_react10.useRef)([]);
|
|
330
|
+
const subscribe = (0, import_react10.useCallback)(
|
|
331
|
+
(onStoreChange) => viewport.store.onChange(onStoreChange),
|
|
332
|
+
[viewport]
|
|
333
|
+
);
|
|
334
|
+
const getSnapshot = (0, import_react10.useCallback)(() => {
|
|
335
|
+
const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();
|
|
336
|
+
const cached = cachedRef.current;
|
|
337
|
+
if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
|
|
338
|
+
return cached;
|
|
339
|
+
}
|
|
340
|
+
cachedRef.current = next;
|
|
341
|
+
return next;
|
|
342
|
+
}, [viewport, type]);
|
|
343
|
+
return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
|
|
344
|
+
}
|
|
154
345
|
// Annotate the CommonJS export names for ESM import in node:
|
|
155
346
|
0 && (module.exports = {
|
|
156
347
|
CanvasElement,
|
|
@@ -158,6 +349,10 @@ function useCamera() {
|
|
|
158
349
|
ViewportContext,
|
|
159
350
|
useActiveTool,
|
|
160
351
|
useCamera,
|
|
352
|
+
useElements,
|
|
353
|
+
useHistory,
|
|
354
|
+
useLayers,
|
|
355
|
+
useToolOptions,
|
|
161
356
|
useViewport
|
|
162
357
|
});
|
|
163
358
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/field-notes-canvas.tsx","../src/context.ts","../src/canvas-element.tsx","../src/use-viewport.ts","../src/use-active-tool.ts","../src/use-camera.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 { useViewport } from './use-viewport';\r\nexport { useActiveTool } from './use-active-tool';\r\nexport { useCamera } from './use-camera';\r\nexport type { CameraState } from './use-camera';\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 './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(): string {\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 return useSyncExternalStore(subscribe, getSnapshot);\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"],"mappings":"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","names":["import_react","FieldNotesCanvas","import_react","import_react","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';\nexport type { FieldNotesCanvasProps, FieldNotesCanvasRef } from './field-notes-canvas';\nexport { CanvasElement } from './canvas-element';\nexport type { CanvasElementProps } from './canvas-element';\nexport {\n useViewport,\n useActiveTool,\n useCamera,\n useToolOptions,\n useLayers,\n useHistory,\n useElements,\n} from './hooks';\nexport type { CameraState, UseLayersResult, UseHistoryResult } from './hooks';\nexport { ViewportContext } from './context';\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';\nimport { createPortal } from 'react-dom';\nimport { useViewport } from './hooks/use-viewport';\n\nexport interface CanvasElementProps {\n position: { x: number; y: number };\n size?: { w: number; h: number };\n children: ReactNode;\n}\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\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\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(\n (name: string) => viewport.toolManager.setTool(name, viewport.toolContext),\n [viewport],\n );\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\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\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\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\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\nexport function useElements(): CanvasElement[];\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\nexport function useElements(type?: ElementType): CanvasElement[] {\n const viewport = useViewport();\n const cachedRef = useRef<CanvasElement[]>([]);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CanvasElement[] => {\n const next = type ? viewport.store.getElementsByType(type) : 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, type]);\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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
-
import { ViewportOptions, Tool, Viewport } from '@fieldnotes/core';
|
|
3
|
+
import { ViewportOptions, Tool, Viewport, Layer, CanvasElement as CanvasElement$1, ElementType } from '@fieldnotes/core';
|
|
4
4
|
|
|
5
5
|
interface FieldNotesCanvasProps {
|
|
6
6
|
options?: ViewportOptions;
|
|
@@ -31,7 +31,7 @@ declare function CanvasElement({ position, size, children }: CanvasElementProps)
|
|
|
31
31
|
|
|
32
32
|
declare function useViewport(): Viewport;
|
|
33
33
|
|
|
34
|
-
declare function useActiveTool(): string;
|
|
34
|
+
declare function useActiveTool(): [toolName: string, setTool: (name: string) => void];
|
|
35
35
|
|
|
36
36
|
interface CameraState {
|
|
37
37
|
x: number;
|
|
@@ -40,6 +40,36 @@ interface CameraState {
|
|
|
40
40
|
}
|
|
41
41
|
declare function useCamera(): CameraState;
|
|
42
42
|
|
|
43
|
+
declare function useToolOptions<T extends Record<string, unknown>>(toolName: string): [options: T | null, setOptions: (partial: Partial<T>) => void];
|
|
44
|
+
|
|
45
|
+
interface UseLayersResult {
|
|
46
|
+
layers: Layer[];
|
|
47
|
+
activeLayerId: string;
|
|
48
|
+
createLayer: (name?: string) => Layer;
|
|
49
|
+
removeLayer: (id: string) => void;
|
|
50
|
+
renameLayer: (id: string, name: string) => void;
|
|
51
|
+
reorderLayer: (id: string, newOrder: number) => void;
|
|
52
|
+
setVisible: (id: string, visible: boolean) => void;
|
|
53
|
+
setLocked: (id: string, locked: boolean) => void;
|
|
54
|
+
setOpacity: (id: string, opacity: number) => void;
|
|
55
|
+
setActiveLayer: (id: string) => void;
|
|
56
|
+
moveElement: (elementId: string, layerId: string) => void;
|
|
57
|
+
}
|
|
58
|
+
declare function useLayers(): UseLayersResult;
|
|
59
|
+
|
|
60
|
+
interface UseHistoryResult {
|
|
61
|
+
canUndo: boolean;
|
|
62
|
+
canRedo: boolean;
|
|
63
|
+
undo: () => void;
|
|
64
|
+
redo: () => void;
|
|
65
|
+
}
|
|
66
|
+
declare function useHistory(): UseHistoryResult;
|
|
67
|
+
|
|
68
|
+
declare function useElements(): CanvasElement$1[];
|
|
69
|
+
declare function useElements<T extends ElementType>(type: T): Extract<CanvasElement$1, {
|
|
70
|
+
type: T;
|
|
71
|
+
}>[];
|
|
72
|
+
|
|
43
73
|
declare const ViewportContext: react.Context<Viewport | null>;
|
|
44
74
|
|
|
45
|
-
export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, ViewportContext, useActiveTool, useCamera, useViewport };
|
|
75
|
+
export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, type UseHistoryResult, type UseLayersResult, ViewportContext, useActiveTool, useCamera, useElements, useHistory, useLayers, useToolOptions, useViewport };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
-
import { ViewportOptions, Tool, Viewport } from '@fieldnotes/core';
|
|
3
|
+
import { ViewportOptions, Tool, Viewport, Layer, CanvasElement as CanvasElement$1, ElementType } from '@fieldnotes/core';
|
|
4
4
|
|
|
5
5
|
interface FieldNotesCanvasProps {
|
|
6
6
|
options?: ViewportOptions;
|
|
@@ -31,7 +31,7 @@ declare function CanvasElement({ position, size, children }: CanvasElementProps)
|
|
|
31
31
|
|
|
32
32
|
declare function useViewport(): Viewport;
|
|
33
33
|
|
|
34
|
-
declare function useActiveTool(): string;
|
|
34
|
+
declare function useActiveTool(): [toolName: string, setTool: (name: string) => void];
|
|
35
35
|
|
|
36
36
|
interface CameraState {
|
|
37
37
|
x: number;
|
|
@@ -40,6 +40,36 @@ interface CameraState {
|
|
|
40
40
|
}
|
|
41
41
|
declare function useCamera(): CameraState;
|
|
42
42
|
|
|
43
|
+
declare function useToolOptions<T extends Record<string, unknown>>(toolName: string): [options: T | null, setOptions: (partial: Partial<T>) => void];
|
|
44
|
+
|
|
45
|
+
interface UseLayersResult {
|
|
46
|
+
layers: Layer[];
|
|
47
|
+
activeLayerId: string;
|
|
48
|
+
createLayer: (name?: string) => Layer;
|
|
49
|
+
removeLayer: (id: string) => void;
|
|
50
|
+
renameLayer: (id: string, name: string) => void;
|
|
51
|
+
reorderLayer: (id: string, newOrder: number) => void;
|
|
52
|
+
setVisible: (id: string, visible: boolean) => void;
|
|
53
|
+
setLocked: (id: string, locked: boolean) => void;
|
|
54
|
+
setOpacity: (id: string, opacity: number) => void;
|
|
55
|
+
setActiveLayer: (id: string) => void;
|
|
56
|
+
moveElement: (elementId: string, layerId: string) => void;
|
|
57
|
+
}
|
|
58
|
+
declare function useLayers(): UseLayersResult;
|
|
59
|
+
|
|
60
|
+
interface UseHistoryResult {
|
|
61
|
+
canUndo: boolean;
|
|
62
|
+
canRedo: boolean;
|
|
63
|
+
undo: () => void;
|
|
64
|
+
redo: () => void;
|
|
65
|
+
}
|
|
66
|
+
declare function useHistory(): UseHistoryResult;
|
|
67
|
+
|
|
68
|
+
declare function useElements(): CanvasElement$1[];
|
|
69
|
+
declare function useElements<T extends ElementType>(type: T): Extract<CanvasElement$1, {
|
|
70
|
+
type: T;
|
|
71
|
+
}>[];
|
|
72
|
+
|
|
43
73
|
declare const ViewportContext: react.Context<Viewport | null>;
|
|
44
74
|
|
|
45
|
-
export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, ViewportContext, useActiveTool, useCamera, useViewport };
|
|
75
|
+
export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, type UseHistoryResult, type UseLayersResult, ViewportContext, useActiveTool, useCamera, useElements, useHistory, useLayers, useToolOptions, useViewport };
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ var FieldNotesCanvas = forwardRef(
|
|
|
46
46
|
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
47
47
|
import { createPortal } from "react-dom";
|
|
48
48
|
|
|
49
|
-
// src/use-viewport.ts
|
|
49
|
+
// src/hooks/use-viewport.ts
|
|
50
50
|
import { useContext } from "react";
|
|
51
51
|
function useViewport() {
|
|
52
52
|
const viewport = useContext(ViewportContext);
|
|
@@ -93,7 +93,7 @@ function CanvasElement({ position, size, children }) {
|
|
|
93
93
|
return createPortal(children, portalTarget);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// src/use-active-tool.ts
|
|
96
|
+
// src/hooks/use-active-tool.ts
|
|
97
97
|
import { useCallback, useSyncExternalStore } from "react";
|
|
98
98
|
function useActiveTool() {
|
|
99
99
|
const viewport = useViewport();
|
|
@@ -102,10 +102,15 @@ function useActiveTool() {
|
|
|
102
102
|
[viewport]
|
|
103
103
|
);
|
|
104
104
|
const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
|
|
105
|
-
|
|
105
|
+
const toolName = useSyncExternalStore(subscribe, getSnapshot);
|
|
106
|
+
const setTool = useCallback(
|
|
107
|
+
(name) => viewport.toolManager.setTool(name, viewport.toolContext),
|
|
108
|
+
[viewport]
|
|
109
|
+
);
|
|
110
|
+
return [toolName, setTool];
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
// src/use-camera.ts
|
|
113
|
+
// src/hooks/use-camera.ts
|
|
109
114
|
import { useCallback as useCallback2, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
110
115
|
function useCamera() {
|
|
111
116
|
const viewport = useViewport();
|
|
@@ -126,12 +131,198 @@ function useCamera() {
|
|
|
126
131
|
}, [viewport]);
|
|
127
132
|
return useSyncExternalStore2(subscribe, getSnapshot);
|
|
128
133
|
}
|
|
134
|
+
|
|
135
|
+
// src/hooks/use-tool-options.ts
|
|
136
|
+
import { useCallback as useCallback3, useRef as useRef4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
137
|
+
function noop() {
|
|
138
|
+
}
|
|
139
|
+
function useToolOptions(toolName) {
|
|
140
|
+
const viewport = useViewport();
|
|
141
|
+
const cachedRef = useRef4(null);
|
|
142
|
+
const subscribe = useCallback3(
|
|
143
|
+
(onStoreChange) => {
|
|
144
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
145
|
+
if (!tool?.onOptionsChange) return noop;
|
|
146
|
+
return tool.onOptionsChange(onStoreChange);
|
|
147
|
+
},
|
|
148
|
+
[viewport, toolName]
|
|
149
|
+
);
|
|
150
|
+
const getSnapshot = useCallback3(() => {
|
|
151
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
152
|
+
if (!tool?.getOptions) return null;
|
|
153
|
+
const next = tool.getOptions();
|
|
154
|
+
const cached = cachedRef.current;
|
|
155
|
+
if (cached && shallowEqual(cached, next)) return cached;
|
|
156
|
+
cachedRef.current = next;
|
|
157
|
+
return next;
|
|
158
|
+
}, [viewport, toolName]);
|
|
159
|
+
const options = useSyncExternalStore3(subscribe, getSnapshot);
|
|
160
|
+
const setOptions = useCallback3(
|
|
161
|
+
(partial) => {
|
|
162
|
+
const tool = viewport.toolManager.getTool(toolName);
|
|
163
|
+
tool?.setOptions?.(partial);
|
|
164
|
+
},
|
|
165
|
+
[viewport, toolName]
|
|
166
|
+
);
|
|
167
|
+
return [options, setOptions];
|
|
168
|
+
}
|
|
169
|
+
function shallowEqual(a, b) {
|
|
170
|
+
const keysA = Object.keys(a);
|
|
171
|
+
const keysB = Object.keys(b);
|
|
172
|
+
if (keysA.length !== keysB.length) return false;
|
|
173
|
+
for (const key of keysA) {
|
|
174
|
+
const valA = a[key];
|
|
175
|
+
const valB = b[key];
|
|
176
|
+
if (valA === valB) continue;
|
|
177
|
+
if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
|
|
178
|
+
const objA = valA;
|
|
179
|
+
const objB = valB;
|
|
180
|
+
const innerKeys = Object.keys(objA);
|
|
181
|
+
if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/hooks/use-layers.ts
|
|
191
|
+
import { useCallback as useCallback4, useRef as useRef5, useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
192
|
+
function useLayers() {
|
|
193
|
+
const viewport = useViewport();
|
|
194
|
+
const cachedRef = useRef5({ layers: [], activeLayerId: "" });
|
|
195
|
+
const subscribe = useCallback4(
|
|
196
|
+
(onStoreChange) => viewport.layerManager.on("change", onStoreChange),
|
|
197
|
+
[viewport]
|
|
198
|
+
);
|
|
199
|
+
const getSnapshot = useCallback4(() => {
|
|
200
|
+
const layers = viewport.layerManager.getLayers();
|
|
201
|
+
const activeLayerId = viewport.layerManager.activeLayerId;
|
|
202
|
+
const cached = cachedRef.current;
|
|
203
|
+
if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
|
|
204
|
+
const next2 = layers[i];
|
|
205
|
+
return next2 !== void 0 && l.id === next2.id && l.name === next2.name && l.visible === next2.visible && l.locked === next2.locked && l.order === next2.order && l.opacity === next2.opacity;
|
|
206
|
+
})) {
|
|
207
|
+
return cached;
|
|
208
|
+
}
|
|
209
|
+
const next = { layers, activeLayerId };
|
|
210
|
+
cachedRef.current = next;
|
|
211
|
+
return next;
|
|
212
|
+
}, [viewport]);
|
|
213
|
+
const snapshot = useSyncExternalStore4(subscribe, getSnapshot);
|
|
214
|
+
const createLayer = useCallback4(
|
|
215
|
+
(name) => viewport.layerManager.createLayer(name),
|
|
216
|
+
[viewport]
|
|
217
|
+
);
|
|
218
|
+
const removeLayer = useCallback4(
|
|
219
|
+
(id) => viewport.layerManager.removeLayer(id),
|
|
220
|
+
[viewport]
|
|
221
|
+
);
|
|
222
|
+
const renameLayer = useCallback4(
|
|
223
|
+
(id, name) => viewport.layerManager.renameLayer(id, name),
|
|
224
|
+
[viewport]
|
|
225
|
+
);
|
|
226
|
+
const reorderLayer = useCallback4(
|
|
227
|
+
(id, newOrder) => viewport.layerManager.reorderLayer(id, newOrder),
|
|
228
|
+
[viewport]
|
|
229
|
+
);
|
|
230
|
+
const setVisible = useCallback4(
|
|
231
|
+
(id, visible) => viewport.layerManager.setLayerVisible(id, visible),
|
|
232
|
+
[viewport]
|
|
233
|
+
);
|
|
234
|
+
const setLocked = useCallback4(
|
|
235
|
+
(id, locked) => viewport.layerManager.setLayerLocked(id, locked),
|
|
236
|
+
[viewport]
|
|
237
|
+
);
|
|
238
|
+
const setOpacity = useCallback4(
|
|
239
|
+
(id, opacity) => viewport.layerManager.setLayerOpacity(id, opacity),
|
|
240
|
+
[viewport]
|
|
241
|
+
);
|
|
242
|
+
const setActiveLayer = useCallback4(
|
|
243
|
+
(id) => viewport.layerManager.setActiveLayer(id),
|
|
244
|
+
[viewport]
|
|
245
|
+
);
|
|
246
|
+
const moveElement = useCallback4(
|
|
247
|
+
(elementId, layerId) => viewport.layerManager.moveElementToLayer(elementId, layerId),
|
|
248
|
+
[viewport]
|
|
249
|
+
);
|
|
250
|
+
return {
|
|
251
|
+
layers: snapshot.layers,
|
|
252
|
+
activeLayerId: snapshot.activeLayerId,
|
|
253
|
+
createLayer,
|
|
254
|
+
removeLayer,
|
|
255
|
+
renameLayer,
|
|
256
|
+
reorderLayer,
|
|
257
|
+
setVisible,
|
|
258
|
+
setLocked,
|
|
259
|
+
setOpacity,
|
|
260
|
+
setActiveLayer,
|
|
261
|
+
moveElement
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/hooks/use-history.ts
|
|
266
|
+
import { useCallback as useCallback5, useRef as useRef6, useSyncExternalStore as useSyncExternalStore5 } from "react";
|
|
267
|
+
function useHistory() {
|
|
268
|
+
const viewport = useViewport();
|
|
269
|
+
const cachedRef = useRef6({ canUndo: false, canRedo: false });
|
|
270
|
+
const subscribe = useCallback5(
|
|
271
|
+
(onStoreChange) => viewport.history.onChange(onStoreChange),
|
|
272
|
+
[viewport]
|
|
273
|
+
);
|
|
274
|
+
const getSnapshot = useCallback5(() => {
|
|
275
|
+
const canUndo = viewport.history.canUndo;
|
|
276
|
+
const canRedo = viewport.history.canRedo;
|
|
277
|
+
const cached = cachedRef.current;
|
|
278
|
+
if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
|
|
279
|
+
return cached;
|
|
280
|
+
}
|
|
281
|
+
const next = { canUndo, canRedo };
|
|
282
|
+
cachedRef.current = next;
|
|
283
|
+
return next;
|
|
284
|
+
}, [viewport]);
|
|
285
|
+
const snapshot = useSyncExternalStore5(subscribe, getSnapshot);
|
|
286
|
+
const undo = useCallback5(() => viewport.undo(), [viewport]);
|
|
287
|
+
const redo = useCallback5(() => viewport.redo(), [viewport]);
|
|
288
|
+
return {
|
|
289
|
+
canUndo: snapshot.canUndo,
|
|
290
|
+
canRedo: snapshot.canRedo,
|
|
291
|
+
undo,
|
|
292
|
+
redo
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/hooks/use-elements.ts
|
|
297
|
+
import { useCallback as useCallback6, useRef as useRef7, useSyncExternalStore as useSyncExternalStore6 } from "react";
|
|
298
|
+
function useElements(type) {
|
|
299
|
+
const viewport = useViewport();
|
|
300
|
+
const cachedRef = useRef7([]);
|
|
301
|
+
const subscribe = useCallback6(
|
|
302
|
+
(onStoreChange) => viewport.store.onChange(onStoreChange),
|
|
303
|
+
[viewport]
|
|
304
|
+
);
|
|
305
|
+
const getSnapshot = useCallback6(() => {
|
|
306
|
+
const next = type ? viewport.store.getElementsByType(type) : viewport.store.getAll();
|
|
307
|
+
const cached = cachedRef.current;
|
|
308
|
+
if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
|
|
309
|
+
return cached;
|
|
310
|
+
}
|
|
311
|
+
cachedRef.current = next;
|
|
312
|
+
return next;
|
|
313
|
+
}, [viewport, type]);
|
|
314
|
+
return useSyncExternalStore6(subscribe, getSnapshot);
|
|
315
|
+
}
|
|
129
316
|
export {
|
|
130
317
|
CanvasElement,
|
|
131
318
|
FieldNotesCanvas,
|
|
132
319
|
ViewportContext,
|
|
133
320
|
useActiveTool,
|
|
134
321
|
useCamera,
|
|
322
|
+
useElements,
|
|
323
|
+
useHistory,
|
|
324
|
+
useLayers,
|
|
325
|
+
useToolOptions,
|
|
135
326
|
useViewport
|
|
136
327
|
};
|
|
137
328
|
//# sourceMappingURL=index.js.map
|
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/use-viewport.ts","../src/use-active-tool.ts","../src/use-camera.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 './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(): string {\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 return useSyncExternalStore(subscribe, getSnapshot);\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"],"mappings":"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","names":["FieldNotesCanvas","useEffect","useRef","useState","useState","useRef","useEffect","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 {\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';\nimport { createPortal } from 'react-dom';\nimport { useViewport } from './hooks/use-viewport';\n\nexport interface CanvasElementProps {\n position: { x: number; y: number };\n size?: { w: number; h: number };\n children: ReactNode;\n}\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\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\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(\n (name: string) => viewport.toolManager.setTool(name, viewport.toolContext),\n [viewport],\n );\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\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\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\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\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\nexport function useElements(): CanvasElement[];\nexport function useElements<T extends ElementType>(type: T): Extract<CanvasElement, { type: T }>[];\nexport function useElements(type?: ElementType): CanvasElement[] {\n const viewport = useViewport();\n const cachedRef = useRef<CanvasElement[]>([]);\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => viewport.store.onChange(onStoreChange),\n [viewport],\n );\n\n const getSnapshot = useCallback((): CanvasElement[] => {\n const next = type ? viewport.store.getElementsByType(type) : 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, type]);\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","useRef","useCallback","useSyncExternalStore"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fieldnotes/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React wrapper for Field Notes canvas SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -33,10 +33,8 @@
|
|
|
33
33
|
"html-embedding",
|
|
34
34
|
"fieldnotes"
|
|
35
35
|
],
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@fieldnotes/core": "0.2.3"
|
|
38
|
-
},
|
|
39
36
|
"peerDependencies": {
|
|
37
|
+
"@fieldnotes/core": ">=0.2.0",
|
|
40
38
|
"react": ">=18",
|
|
41
39
|
"react-dom": ">=18"
|
|
42
40
|
},
|
|
@@ -49,7 +47,8 @@
|
|
|
49
47
|
"react": "^19.2.4",
|
|
50
48
|
"react-dom": "^19.2.4",
|
|
51
49
|
"tsup": "^8.5.1",
|
|
52
|
-
"vitest": "^4.1.0"
|
|
50
|
+
"vitest": "^4.1.0",
|
|
51
|
+
"@fieldnotes/core": "0.8.4"
|
|
53
52
|
},
|
|
54
53
|
"scripts": {
|
|
55
54
|
"build": "tsup",
|