@fieldnotes/react 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,171 @@
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 React components are fully interactive — clicks, inputs, forms all work normally.
60
+
61
+ ## Hooks
62
+
63
+ ### `useViewport()`
64
+
65
+ Access the core `Viewport` instance for imperative operations:
66
+
67
+ ```tsx
68
+ import { useViewport } from '@fieldnotes/react';
69
+
70
+ function Toolbar() {
71
+ const viewport = useViewport();
72
+
73
+ return <button onClick={() => viewport.undo()}>Undo</button>;
74
+ }
75
+ ```
76
+
77
+ Must be used inside `<FieldNotesCanvas>`.
78
+
79
+ ### `useActiveTool()`
80
+
81
+ Reactive current tool name — re-renders when the tool changes:
82
+
83
+ ```tsx
84
+ import { useActiveTool, useViewport } from '@fieldnotes/react';
85
+
86
+ function ToolIndicator() {
87
+ const tool = useActiveTool();
88
+ const viewport = useViewport();
89
+
90
+ return (
91
+ <div>
92
+ <span>Current: {tool}</span>
93
+ <button onClick={() => viewport.toolManager.setTool('pencil', viewport.toolContext)}>
94
+ Pencil
95
+ </button>
96
+ </div>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ### `useCamera()`
102
+
103
+ Reactive camera state (position + zoom) — re-renders on pan/zoom:
104
+
105
+ ```tsx
106
+ import { useCamera } from '@fieldnotes/react';
107
+
108
+ function CameraInfo() {
109
+ const { x, y, zoom } = useCamera();
110
+
111
+ return (
112
+ <span>
113
+ {zoom.toFixed(2)}x at ({x.toFixed(0)}, {y.toFixed(0)})
114
+ </span>
115
+ );
116
+ }
117
+ ```
118
+
119
+ ## Component API
120
+
121
+ ### `<FieldNotesCanvas>`
122
+
123
+ | Prop | Type | Description |
124
+ | ------------- | ------------------------------ | --------------------------------------- |
125
+ | `options` | `ViewportOptions` | Camera and background config |
126
+ | `tools` | `Tool[]` | Tools to register on mount |
127
+ | `defaultTool` | `string` | Tool to activate on mount |
128
+ | `className` | `string` | CSS class for the container div |
129
+ | `style` | `CSSProperties` | Inline styles for the container div |
130
+ | `onReady` | `(viewport: Viewport) => void` | Called after Viewport is created |
131
+ | `children` | `ReactNode` | Child components (have access to hooks) |
132
+ | `ref` | `Ref<FieldNotesCanvasRef>` | Exposes `{ viewport }` |
133
+
134
+ ### `<CanvasElement>`
135
+
136
+ | Prop | Type | Default | Description |
137
+ | ---------- | -------------------------- | -------------------- | ---------------------------------- |
138
+ | `position` | `{ x: number; y: number }` | required | World-space position |
139
+ | `size` | `{ w: number; h: number }` | `{ w: 200, h: 150 }` | Element size in world-space pixels |
140
+ | `children` | `ReactNode` | required | React content to render on canvas |
141
+
142
+ Position and size updates are reactive — change the props and the element moves/resizes on the canvas.
143
+
144
+ ## Accessing the Viewport Directly
145
+
146
+ For advanced use cases, use a ref:
147
+
148
+ ```tsx
149
+ import { useRef } from 'react';
150
+ import { FieldNotesCanvas, type FieldNotesCanvasRef } from '@fieldnotes/react';
151
+
152
+ function App() {
153
+ const canvasRef = useRef<FieldNotesCanvasRef>(null);
154
+
155
+ const exportState = () => {
156
+ const json = canvasRef.current?.viewport?.exportJSON();
157
+ console.log(json);
158
+ };
159
+
160
+ return (
161
+ <>
162
+ <FieldNotesCanvas ref={canvasRef} style={{ width: '100vw', height: '100vh' }} />
163
+ <button onClick={exportState}>Export</button>
164
+ </>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
package/dist/index.cjs CHANGED
@@ -20,12 +20,144 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- VERSION: () => VERSION
23
+ CanvasElement: () => CanvasElement,
24
+ FieldNotesCanvas: () => FieldNotesCanvas,
25
+ ViewportContext: () => ViewportContext,
26
+ useActiveTool: () => useActiveTool,
27
+ useCamera: () => useCamera,
28
+ useViewport: () => useViewport
24
29
  });
25
30
  module.exports = __toCommonJS(index_exports);
26
- var VERSION = "0.1.0";
31
+
32
+ // src/field-notes-canvas.tsx
33
+ var import_react2 = require("react");
34
+ var import_core = require("@fieldnotes/core");
35
+
36
+ // src/context.ts
37
+ var import_react = require("react");
38
+ var ViewportContext = (0, import_react.createContext)(null);
39
+
40
+ // src/field-notes-canvas.tsx
41
+ var import_jsx_runtime = require("react/jsx-runtime");
42
+ var FieldNotesCanvas = (0, import_react2.forwardRef)(
43
+ function FieldNotesCanvas2({ options, tools, defaultTool, className, style, children, onReady }, ref) {
44
+ const containerRef = (0, import_react2.useRef)(null);
45
+ const [viewport, setViewport] = (0, import_react2.useState)(null);
46
+ (0, import_react2.useImperativeHandle)(ref, () => ({ viewport }), [viewport]);
47
+ (0, import_react2.useEffect)(() => {
48
+ const el = containerRef.current;
49
+ if (!el) return;
50
+ const vp = new import_core.Viewport(el, options);
51
+ if (tools) {
52
+ for (const tool of tools) {
53
+ vp.toolManager.register(tool);
54
+ }
55
+ }
56
+ if (defaultTool) {
57
+ vp.toolManager.setTool(defaultTool, vp.toolContext);
58
+ }
59
+ setViewport(vp);
60
+ onReady?.(vp);
61
+ return () => {
62
+ vp.destroy();
63
+ setViewport(null);
64
+ };
65
+ }, []);
66
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: containerRef, className, style, children: viewport && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ViewportContext.Provider, { value: viewport, children }) });
67
+ }
68
+ );
69
+
70
+ // src/canvas-element.tsx
71
+ var import_react4 = require("react");
72
+ var import_react_dom = require("react-dom");
73
+
74
+ // src/use-viewport.ts
75
+ var import_react3 = require("react");
76
+ function useViewport() {
77
+ const viewport = (0, import_react3.useContext)(ViewportContext);
78
+ if (!viewport) {
79
+ throw new Error("useViewport must be used inside <FieldNotesCanvas>");
80
+ }
81
+ return viewport;
82
+ }
83
+
84
+ // src/canvas-element.tsx
85
+ function CanvasElement({ position, size, children }) {
86
+ const viewport = useViewport();
87
+ const [portalTarget, setPortalTarget] = (0, import_react4.useState)(null);
88
+ const elementIdRef = (0, import_react4.useRef)(null);
89
+ (0, import_react4.useEffect)(() => {
90
+ const container = document.createElement("div");
91
+ Object.assign(container.style, {
92
+ width: "100%",
93
+ height: "100%"
94
+ });
95
+ viewport.domLayer.appendChild(container);
96
+ const id = viewport.addHtmlElement(container, position, size);
97
+ elementIdRef.current = id;
98
+ setPortalTarget(container);
99
+ return () => {
100
+ if (elementIdRef.current) {
101
+ viewport.store.remove(elementIdRef.current);
102
+ viewport.requestRender();
103
+ elementIdRef.current = null;
104
+ }
105
+ setPortalTarget(null);
106
+ };
107
+ }, [viewport]);
108
+ (0, import_react4.useEffect)(() => {
109
+ const id = elementIdRef.current;
110
+ if (!id) return;
111
+ viewport.store.update(id, { position });
112
+ if (size) {
113
+ viewport.store.update(id, { size });
114
+ }
115
+ viewport.requestRender();
116
+ }, [viewport, position.x, position.y, size?.w, size?.h]);
117
+ if (!portalTarget) return null;
118
+ return (0, import_react_dom.createPortal)(children, portalTarget);
119
+ }
120
+
121
+ // src/use-active-tool.ts
122
+ var import_react5 = require("react");
123
+ function useActiveTool() {
124
+ const viewport = useViewport();
125
+ const subscribe = (0, import_react5.useCallback)(
126
+ (onStoreChange) => viewport.toolManager.onChange(() => onStoreChange()),
127
+ [viewport]
128
+ );
129
+ const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
130
+ return (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot);
131
+ }
132
+
133
+ // src/use-camera.ts
134
+ var import_react6 = require("react");
135
+ function useCamera() {
136
+ const viewport = useViewport();
137
+ const cachedRef = (0, import_react6.useRef)({ x: 0, y: 0, zoom: 1 });
138
+ const subscribe = (0, import_react6.useCallback)(
139
+ (onStoreChange) => viewport.camera.onChange(onStoreChange),
140
+ [viewport]
141
+ );
142
+ const getSnapshot = (0, import_react6.useCallback)(() => {
143
+ const { position, zoom } = viewport.camera;
144
+ const cached = cachedRef.current;
145
+ if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
146
+ return cached;
147
+ }
148
+ const next = { x: position.x, y: position.y, zoom };
149
+ cachedRef.current = next;
150
+ return next;
151
+ }, [viewport]);
152
+ return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot);
153
+ }
27
154
  // Annotate the CommonJS export names for ESM import in node:
28
155
  0 && (module.exports = {
29
- VERSION
156
+ CanvasElement,
157
+ FieldNotesCanvas,
158
+ ViewportContext,
159
+ useActiveTool,
160
+ useCamera,
161
+ useViewport
30
162
  });
31
163
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const VERSION = '0.1.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,UAAU;","names":[]}
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"]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,45 @@
1
- declare const VERSION = "0.1.0";
1
+ import * as react from 'react';
2
+ import { CSSProperties, ReactNode } from 'react';
3
+ import { ViewportOptions, Tool, Viewport } from '@fieldnotes/core';
2
4
 
3
- export { VERSION };
5
+ interface FieldNotesCanvasProps {
6
+ options?: ViewportOptions;
7
+ tools?: Tool[];
8
+ defaultTool?: string;
9
+ className?: string;
10
+ style?: CSSProperties;
11
+ children?: ReactNode;
12
+ onReady?: (viewport: Viewport) => void;
13
+ }
14
+ interface FieldNotesCanvasRef {
15
+ viewport: Viewport | null;
16
+ }
17
+ declare const FieldNotesCanvas: react.ForwardRefExoticComponent<FieldNotesCanvasProps & react.RefAttributes<FieldNotesCanvasRef>>;
18
+
19
+ interface CanvasElementProps {
20
+ position: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ size?: {
25
+ w: number;
26
+ h: number;
27
+ };
28
+ children: ReactNode;
29
+ }
30
+ declare function CanvasElement({ position, size, children }: CanvasElementProps): react.ReactPortal | null;
31
+
32
+ declare function useViewport(): Viewport;
33
+
34
+ declare function useActiveTool(): string;
35
+
36
+ interface CameraState {
37
+ x: number;
38
+ y: number;
39
+ zoom: number;
40
+ }
41
+ declare function useCamera(): CameraState;
42
+
43
+ declare const ViewportContext: react.Context<Viewport | null>;
44
+
45
+ export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, ViewportContext, useActiveTool, useCamera, useViewport };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,45 @@
1
- declare const VERSION = "0.1.0";
1
+ import * as react from 'react';
2
+ import { CSSProperties, ReactNode } from 'react';
3
+ import { ViewportOptions, Tool, Viewport } from '@fieldnotes/core';
2
4
 
3
- export { VERSION };
5
+ interface FieldNotesCanvasProps {
6
+ options?: ViewportOptions;
7
+ tools?: Tool[];
8
+ defaultTool?: string;
9
+ className?: string;
10
+ style?: CSSProperties;
11
+ children?: ReactNode;
12
+ onReady?: (viewport: Viewport) => void;
13
+ }
14
+ interface FieldNotesCanvasRef {
15
+ viewport: Viewport | null;
16
+ }
17
+ declare const FieldNotesCanvas: react.ForwardRefExoticComponent<FieldNotesCanvasProps & react.RefAttributes<FieldNotesCanvasRef>>;
18
+
19
+ interface CanvasElementProps {
20
+ position: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ size?: {
25
+ w: number;
26
+ h: number;
27
+ };
28
+ children: ReactNode;
29
+ }
30
+ declare function CanvasElement({ position, size, children }: CanvasElementProps): react.ReactPortal | null;
31
+
32
+ declare function useViewport(): Viewport;
33
+
34
+ declare function useActiveTool(): string;
35
+
36
+ interface CameraState {
37
+ x: number;
38
+ y: number;
39
+ zoom: number;
40
+ }
41
+ declare function useCamera(): CameraState;
42
+
43
+ declare const ViewportContext: react.Context<Viewport | null>;
44
+
45
+ export { type CameraState, CanvasElement, type CanvasElementProps, FieldNotesCanvas, type FieldNotesCanvasProps, type FieldNotesCanvasRef, ViewportContext, useActiveTool, useCamera, useViewport };
package/dist/index.js CHANGED
@@ -1,6 +1,137 @@
1
- // src/index.ts
2
- var VERSION = "0.1.0";
1
+ // src/field-notes-canvas.tsx
2
+ import {
3
+ useRef,
4
+ useEffect,
5
+ useState,
6
+ forwardRef,
7
+ useImperativeHandle
8
+ } from "react";
9
+ import { Viewport } from "@fieldnotes/core";
10
+
11
+ // src/context.ts
12
+ import { createContext } from "react";
13
+ var ViewportContext = createContext(null);
14
+
15
+ // src/field-notes-canvas.tsx
16
+ import { jsx } from "react/jsx-runtime";
17
+ var FieldNotesCanvas = forwardRef(
18
+ function FieldNotesCanvas2({ options, tools, defaultTool, className, style, children, onReady }, ref) {
19
+ const containerRef = useRef(null);
20
+ const [viewport, setViewport] = useState(null);
21
+ useImperativeHandle(ref, () => ({ viewport }), [viewport]);
22
+ useEffect(() => {
23
+ const el = containerRef.current;
24
+ if (!el) return;
25
+ const vp = new Viewport(el, options);
26
+ if (tools) {
27
+ for (const tool of tools) {
28
+ vp.toolManager.register(tool);
29
+ }
30
+ }
31
+ if (defaultTool) {
32
+ vp.toolManager.setTool(defaultTool, vp.toolContext);
33
+ }
34
+ setViewport(vp);
35
+ onReady?.(vp);
36
+ return () => {
37
+ vp.destroy();
38
+ setViewport(null);
39
+ };
40
+ }, []);
41
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style, children: viewport && /* @__PURE__ */ jsx(ViewportContext.Provider, { value: viewport, children }) });
42
+ }
43
+ );
44
+
45
+ // src/canvas-element.tsx
46
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
47
+ import { createPortal } from "react-dom";
48
+
49
+ // src/use-viewport.ts
50
+ import { useContext } from "react";
51
+ function useViewport() {
52
+ const viewport = useContext(ViewportContext);
53
+ if (!viewport) {
54
+ throw new Error("useViewport must be used inside <FieldNotesCanvas>");
55
+ }
56
+ return viewport;
57
+ }
58
+
59
+ // src/canvas-element.tsx
60
+ function CanvasElement({ position, size, children }) {
61
+ const viewport = useViewport();
62
+ const [portalTarget, setPortalTarget] = useState2(null);
63
+ const elementIdRef = useRef2(null);
64
+ useEffect2(() => {
65
+ const container = document.createElement("div");
66
+ Object.assign(container.style, {
67
+ width: "100%",
68
+ height: "100%"
69
+ });
70
+ viewport.domLayer.appendChild(container);
71
+ const id = viewport.addHtmlElement(container, position, size);
72
+ elementIdRef.current = id;
73
+ setPortalTarget(container);
74
+ return () => {
75
+ if (elementIdRef.current) {
76
+ viewport.store.remove(elementIdRef.current);
77
+ viewport.requestRender();
78
+ elementIdRef.current = null;
79
+ }
80
+ setPortalTarget(null);
81
+ };
82
+ }, [viewport]);
83
+ useEffect2(() => {
84
+ const id = elementIdRef.current;
85
+ if (!id) return;
86
+ viewport.store.update(id, { position });
87
+ if (size) {
88
+ viewport.store.update(id, { size });
89
+ }
90
+ viewport.requestRender();
91
+ }, [viewport, position.x, position.y, size?.w, size?.h]);
92
+ if (!portalTarget) return null;
93
+ return createPortal(children, portalTarget);
94
+ }
95
+
96
+ // src/use-active-tool.ts
97
+ import { useCallback, useSyncExternalStore } from "react";
98
+ function useActiveTool() {
99
+ const viewport = useViewport();
100
+ const subscribe = useCallback(
101
+ (onStoreChange) => viewport.toolManager.onChange(() => onStoreChange()),
102
+ [viewport]
103
+ );
104
+ const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
105
+ return useSyncExternalStore(subscribe, getSnapshot);
106
+ }
107
+
108
+ // src/use-camera.ts
109
+ import { useCallback as useCallback2, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
110
+ function useCamera() {
111
+ const viewport = useViewport();
112
+ const cachedRef = useRef3({ x: 0, y: 0, zoom: 1 });
113
+ const subscribe = useCallback2(
114
+ (onStoreChange) => viewport.camera.onChange(onStoreChange),
115
+ [viewport]
116
+ );
117
+ const getSnapshot = useCallback2(() => {
118
+ const { position, zoom } = viewport.camera;
119
+ const cached = cachedRef.current;
120
+ if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
121
+ return cached;
122
+ }
123
+ const next = { x: position.x, y: position.y, zoom };
124
+ cachedRef.current = next;
125
+ return next;
126
+ }, [viewport]);
127
+ return useSyncExternalStore2(subscribe, getSnapshot);
128
+ }
3
129
  export {
4
- VERSION
130
+ CanvasElement,
131
+ FieldNotesCanvas,
132
+ ViewportContext,
133
+ useActiveTool,
134
+ useCamera,
135
+ useViewport
5
136
  };
6
137
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const VERSION = '0.1.0';\n"],"mappings":";AAAO,IAAM,UAAU;","names":[]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "React wrapper for Field Notes canvas SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -30,6 +30,7 @@
30
30
  "react-dom": ">=18"
31
31
  },
32
32
  "devDependencies": {
33
+ "@testing-library/react": "^16.3.2",
33
34
  "@types/react": "^19.2.14",
34
35
  "@types/react-dom": "^19.2.3",
35
36
  "@vitest/coverage-v8": "^4.1.0",